Application is meant to help teams collaborate in a working environment, having a data storage, vcs, facebook, google hangout and a file upload feature
What we are trying to Achieve 6025 { /** * Created by user on 01/07/2016. */ /** * @license OpenTok.js v2.8.0 14c8b62 HEAD * * Copyright (c) 2010-2016 TokBox, Inc. * Subject to the applicable Software Development Kit (SDK) License Agreement: * https://tokbox.com/support/sdk_license * * Date: April 17 18:37:35 2016 */ (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oon, once, and off
* methods of objects that can dispatch events.
*
* @class EventDispatcher
*/
module.exports = function eventing(self, syncronous) {
var innerEventing = (nodeEventing || browserEventing)(this, syncronous);
/**
* Adds an event handler function for one or more events.
*
* * The following code adds an event handler for one event: *
* *
* obj.on("eventName", function (event) {
* // This is the event handler.
* });
*
*
* If you pass in multiple event names and a handler method, the handler is * registered for each of those events:
* *
* obj.on("eventName1 eventName2",
* function (event) {
* // This is the event handler.
* });
*
*
* You can also pass in a third context parameter (which is optional) to
* define the value of this in the handler method:
obj.on("eventName",
* function (event) {
* // This is the event handler.
* },
* obj);
*
*
* * The method also supports an alternate syntax, in which the first parameter is an object * that is a hash map of event names and handler functions and the second parameter (optional) * is the context for this in each handler: *
*
* obj.on(
* {
* eventName1: function (event) {
* // This is the handler for eventName1.
* },
* eventName2: function (event) {
* // This is the handler for eventName2.
* }
* },
* obj);
*
*
* * If you do not add a handler for an event, the event is ignored locally. *
* * @param {String} type The string identifying the type of event. You can specify multiple event * names in this string, separating them with a space. The event handler will process each of * the events. * @param {Function} handler The handler function to process the event. This function takes * the event object as a parameter. * @param {Object} context (Optional) Defines the value ofthis in the event
* handler function.
*
* @returns {EventDispatcher} The EventDispatcher object.
*
* @memberOf EventDispatcher
* @method #on
* @see off()
* @see once()
* @see Events
*/
self.on = function(eventNames, handlerOrContext, context) {
if (typeof(eventNames) === 'string' && handlerOrContext) {
innerEventing.addListeners(eventNames.split(' '), handlerOrContext, context);
}
else {
for (var name in eventNames) {
if (eventNames.hasOwnProperty(name)) {
innerEventing.addListeners([name], eventNames[name], handlerOrContext);
}
}
}
return this;
};
/**
* Removes an event handler or handlers.
*
* If you pass in one event name and a handler method, the handler is removed for that * event:
* *obj.off("eventName", eventHandler);
*
* If you pass in multiple event names and a handler method, the handler is removed for * those events:
* *obj.off("eventName1 eventName2", eventHandler);
*
* If you pass in an event name (or names) and no handler method, all handlers are * removed for those events:
* *obj.off("event1Name event2Name");
*
* If you pass in no arguments, all event handlers are removed for all events * dispatched by the object:
* *obj.off();* *
* The method also supports an alternate syntax, in which the first parameter is an object that * is a hash map of event names and handler functions and the second parameter (optional) is * the context for this in each handler: *
*
* obj.off(
* {
* eventName1: event1Handler,
* eventName2: event2Handler
* });
*
*
* @param {String} type (Optional) The string identifying the type of event. You can
* use a space to specify multiple events, as in "accessAllowed accessDenied
* accessDialogClosed". If you pass in no type value (or other arguments),
* all event handlers are removed for the object.
* @param {Function} handler (Optional) The event handler function to remove. The handler
* must be the same function object as was passed into on(). Be careful with
* helpers like bind() that return a new function when called. If you pass in
* no handler, all event handlers are removed for the specified event
* type.
* @param {Object} context (Optional) If you specify a context, the event handler
* is removed for all specified events and handlers that use the specified context. (The
* context must match the context passed into on().)
*
* @returns {Object} The object that dispatched the event.
*
* @memberOf EventDispatcher
* @method #off
* @see on()
* @see once()
* @see Events
*/
self.off = function(eventNames, handlerOrContext, context) {
if (typeof eventNames === 'string') {
if (handlerOrContext && util.isFunction(handlerOrContext)) {
innerEventing.removeListeners(eventNames.split(' '), handlerOrContext, context);
}
else {
innerEventing.removeAllListenersNamed(eventNames.split(' '));
}
} else if (!eventNames) {
innerEventing.removeAllListeners();
} else {
for (var name in eventNames) {
if (eventNames.hasOwnProperty(name)) {
innerEventing.removeListeners([name], eventNames[name], handlerOrContext);
}
}
}
return this;
};
/**
* Adds an event handler function for one or more events. Once the handler is called,
* the specified handler method is removed as a handler for this event. (When you use
* the on() method to add an event handler, the handler is not
* removed when it is called.) The once() method is the equivilent of
* calling the on()
* method and calling off() the first time the handler is invoked.
*
*
* The following code adds a one-time event handler for the accessAllowed event:
*
* obj.once("eventName", function (event) {
* // This is the event handler.
* });
*
*
* If you pass in multiple event names and a handler method, the handler is registered * for each of those events:
* *obj.once("eventName1 eventName2"
* function (event) {
* // This is the event handler.
* });
*
*
* You can also pass in a third context parameter (which is optional) to define
* the value of
* this in the handler method:
obj.once("eventName",
* function (event) {
* // This is the event handler.
* },
* obj);
*
*
* * The method also supports an alternate syntax, in which the first parameter is an object that * is a hash map of event names and handler functions and the second parameter (optional) is the * context for this in each handler: *
*
* obj.once(
* {
* eventName1: function (event) {
* // This is the event handler for eventName1.
* },
* eventName2: function (event) {
* // This is the event handler for eventName1.
* }
* },
* obj);
*
*
* @param {String} type The string identifying the type of event. You can specify multiple
* event names in this string, separating them with a space. The event handler will process
* the first occurence of the events. After the first event, the handler is removed (for
* all specified events).
* @param {Function} handler The handler function to process the event. This function takes
* the event object as a parameter.
* @param {Object} context (Optional) Defines the value of this in the event
* handler function.
*
* @returns {Object} The object that dispatched the event.
*
* @memberOf EventDispatcher
* @method #once
* @see on()
* @see off()
* @see Events
*/
self.once = function(eventNames, handler, context) {
var handleThisOnce = function() {
self.off(eventNames, handleThisOnce, context);
handler.apply(context, arguments);
};
handleThisOnce.originalHandler = handler;
self.on(eventNames, handleThisOnce, context);
return this;
};
// Execute any listeners bound to the +event+ Event.
//
// Each handler will be executed async. On completion the defaultAction
// handler will be executed with the args.
//
// @param [Event] event
// An Event object.
//
// @param [Function, Null, Undefined] defaultAction
// An optional function to execute after every other handler. This will execute even
// if there are listeners bound to this event. +defaultAction+ will be passed
// args as a normal handler would.
//
// @return this
//
self.dispatchEvent = function(event, defaultAction) {
if (!event.type) {
logging.error('OTHelpers.Eventing.dispatchEvent: Event has no type');
logging.error(event);
throw new Error('OTHelpers.Eventing.dispatchEvent: Event has no type');
}
if (!event.target) {
event.target = this;
}
innerEventing.dispatchEvent(event, defaultAction);
return this;
};
// Execute each handler for the event called +name+.
//
// Each handler will be executed async, and any exceptions that they throw will
// be caught and logged
//
// How to pass these?
// * defaultAction
//
// @example
// foo.on('bar', function(name, message) {
// alert("Hello " + name + ": " + message);
// });
//
// foo.trigger('OpenTok', 'asdf'); // -> Hello OpenTok: asdf
//
//
// @param [String] eventName
// The name of this event.
//
// @param [Array] arguments
// Any additional arguments beyond +eventName+ will be passed to the handlers.
//
// @return this
//
self.trigger = function(/* eventName [, arg0, arg1, ..., argN ] */) {
innerEventing.trigger.apply(innerEventing, arguments);
return this;
};
// Alias of trigger for easier node compatibility
self.emit = self.trigger;
/**
* Deprecated; use on() or once() instead.
* * This method registers a method as an event listener for a specific event. *
* *
* If a handler is not registered for an event, the event is ignored locally. If the * event listener function does not exist, the event is ignored locally. *
*
* Throws an exception if the listener name is invalid.
*
this in the event
* handler function.
*
* @memberOf EventDispatcher
* @method #addEventListener
* @see on()
* @see once()
* @see Events
*/
// See 'on' for usage.
// @depreciated will become a private helper function in the future.
self.addEventListener = function(eventName, handler, context) {
logging.warn('The addEventListener() method is deprecated. Use on() or once() instead.');
return self.on(eventName, handler, context);
};
/**
* Deprecated; use off() instead.
* * Removes an event listener for a specific event. *
* *
* Throws an exception if the listener name is invalid.
*
context, the event
* handler is removed for all specified events and event listeners that use the specified
context. (The context must match the context passed into
* addEventListener().)
*
* @memberOf EventDispatcher
* @method #removeEventListener
* @see off()
* @see Events
*/
// See 'off' for usage.
// @depreciated will become a private helper function in the future.
self.removeEventListener = function(eventName, handler, context) {
logging.warn('The removeEventListener() method is deprecated. Use off() instead.');
return self.off(eventName, handler, context);
};
// We expose the inner eventing for testing purposes.
if (!self.__testOnly) {
self.__testOnly = {};
}
self.__testOnly.innerEventing = innerEventing;
return self;
};
},{"../logging":29,"../util":34,"./eventing/browser":8,"./eventing/node":10}],8:[function(require,module,exports){
'use strict';
var env = require('../../env');
var callbacks = require('../../callbacks');
if(env.name !== 'Node') {
module.exports = function browserEventing(self, syncronous) {
var api = {
events: {}
};
// Call the defaultAction, passing args
function executeDefaultAction(defaultAction, args) {
if (!defaultAction) return;
defaultAction.apply(null, args.slice());
}
// Execute each handler in +listeners+ with +args+.
//
// Each handler will be executed async. On completion the defaultAction
// handler will be executed with the args.
//
// @param [Array] listeners
// An array of functions to execute. Each will be passed args.
//
// @param [Array] args
// An array of arguments to execute each function in +listeners+ with.
//
// @param [String] name
// The name of this event.
//
// @param [Function, Null, Undefined] defaultAction
// An optional function to execute after every other handler. This will execute even
// if +listeners+ is empty. +defaultAction+ will be passed args as a normal
// handler would.
//
// @return Undefined
//
function executeListenersAsyncronously(name, args, defaultAction) {
var listeners = api.events[name];
if (!listeners || listeners.length === 0) return;
var listenerAcks = listeners.length;
listeners.forEach(function(listener) { // , index
function filterHandlers(_listener) {
return _listener.handler === listener.handler;
}
// We run this asynchronously so that it doesn't interfere with execution if an
// error happens
callbacks.callAsync(function() {
try {
// have to check if the listener has not been removed
if (api.events[name] && api.events[name].some(filterHandlers)) {
(listener.closure || listener.handler).apply(listener.context || null, args);
}
}
finally {
listenerAcks--;
if (listenerAcks === 0) {
executeDefaultAction(defaultAction, args);
}
}
});
});
}
// This is identical to executeListenersAsyncronously except that handlers will
// be executed syncronously.
//
// On completion the defaultAction handler will be executed with the args.
//
// @param [Array] listeners
// An array of functions to execute. Each will be passed args.
//
// @param [Array] args
// An array of arguments to execute each function in +listeners+ with.
//
// @param [String] name
// The name of this event.
//
// @param [Function, Null, Undefined] defaultAction
// An optional function to execute after every other handler. This will execute even
// if +listeners+ is empty. +defaultAction+ will be passed args as a normal
// handler would.
//
// @return Undefined
//
function executeListenersSyncronously(name, args) { // defaultAction is not used
var listeners = api.events[name];
if (!listeners || listeners.length === 0) return;
listeners.forEach(function(listener) { // index
(listener.closure || listener.handler).apply(listener.context || null, args);
});
}
var executeListeners = syncronous === true ?
executeListenersSyncronously : executeListenersAsyncronously;
api.addListeners = function (eventNames, handler, context, closure) {
var listener = {handler: handler};
if (context) listener.context = context;
if (closure) listener.closure = closure;
eventNames.forEach(function(name) {
if (!api.events[name]) api.events[name] = [];
api.events[name].push(listener);
var addedListener = name + ':added';
if (api.events[addedListener]) {
executeListeners(addedListener, [api.events[name].length]);
}
});
};
api.removeListeners = function(eventNames, handler, context) {
function filterListeners(listener) {
var isCorrectHandler = (
listener.handler.originalHandler === handler ||
listener.handler === handler
);
return !(isCorrectHandler && listener.context === context);
}
eventNames.forEach(function(name) {
if (api.events[name]) {
api.events[name] = api.events[name].filter(filterListeners);
if (api.events[name].length === 0) delete api.events[name];
var removedListener = name + ':removed';
if (api.events[ removedListener]) {
executeListeners(removedListener, [api.events[name] ? api.events[name].length : 0]);
}
}
});
};
api.removeAllListenersNamed = function (eventNames) {
eventNames.forEach(function(name) {
if (api.events[name]) {
delete api.events[name];
}
});
};
api.removeAllListeners = function () {
api.events = {};
};
api.dispatchEvent = function(event, defaultAction) {
if (!api.events[event.type] || api.events[event.type].length === 0) {
executeDefaultAction(defaultAction, [event]);
return;
}
executeListeners(event.type, [event], defaultAction);
};
api.trigger = function(/* eventName [, arg1, arg2, ...argN] */) {
var args = Array.prototype.slice.call(arguments);
var eventName = args.shift();
if (!api.events[eventName] || api.events[eventName].length === 0) {
return;
}
executeListeners(eventName, args);
};
return api;
};
}
},{"../../callbacks":12,"../../env":25}],9:[function(require,module,exports){
'use strict';
var logging = require('../../logging');
module.exports = function Event() {
return function (type, cancelable) {
this.type = type;
this.cancelable = cancelable !== undefined ? cancelable : true;
var _defaultPrevented = false;
this.preventDefault = function() {
if (this.cancelable) {
_defaultPrevented = true;
} else {
logging.warn('Event.preventDefault :: Trying to preventDefault ' +
'on an Event that isn\'t cancelable');
}
};
this.isDefaultPrevented = function() {
return _defaultPrevented;
};
};
};
},{"../../logging":29}],10:[function(require,module,exports){
'use strict';
var env = require('../../env');
if (env.name === 'Node') {
var EventEmitter = require('events').EventEmitter,
nodeUtil = require('util');
// container for the EventEmitter behaviour. This prevents tight coupling
// caused by accidentally bleeding implementation details and API into whatever
// objects nodeEventing is applied to.
var NodeEventable = function NodeEventable() {
EventEmitter.call(this);
this.events = {};
};
nodeUtil.inherits(NodeEventable, EventEmitter);
module.exports = function nodeEventing(/* self */) {
var api = new NodeEventable();
api.addListeners = function (eventNames, handler, context, closure) {
var listener = {handler: handler};
if (context) listener.context = context;
if (closure) listener.closure = closure;
eventNames.forEach(function(name) {
if (!api.events[name]) api.events[name] = [];
api.events[name].push(listener);
api.on(name, handler);
var addedListener = name + ':added';
if (api.events[addedListener]) {
api.emit(addedListener, api.events[name].length);
}
});
};
api.removeAllListenersNamed = function (eventNames) {
var _eventNames = eventNames.split(' ');
api.removeAllListeners(_eventNames);
_eventNames.forEach(function(name) {
if (api.events[name]) delete api.events[name];
});
};
api.removeListeners = function (eventNames, handler, closure) {
function filterHandlers(listener) {
return !(listener.handler === handler && listener.closure === closure);
}
eventNames.split(' ').forEach(function(name) {
if (api.events[name]) {
api.off(name, handler);
api.events[name] = api.events[name].filter(filterHandlers);
if (api.events[name].length === 0) delete api.events[name];
var removedListener = name + ':removed';
if (api.events[removedListener]) {
api.emit(removedListener, api.events[name] ? api.events[name].length : 0);
}
}
});
};
api.removeAllListeners = function () {
api.events = {};
api.removeAllListeners();
};
api.dispatchEvent = function(event, defaultAction) {
this.emit(event.type, event);
if (defaultAction) {
defaultAction.call(null, event);
}
};
api.trigger = api.emit.bind(api);
return api;
};
}
else {
module.exports = undefined;
}
},{"../../env":25,"events":72,"util":77}],11:[function(require,module,exports){
'use strict';
var util = require('../util');
module.exports = function statable(
self,
possibleStates,
initialState,
stateChanged,
stateChangedFailed
) {
var previousState,
currentState = self.currentState = initialState;
var setState = function(state) {
if (currentState !== state) {
if (possibleStates.indexOf(state) === -1) {
if (stateChangedFailed && util.isFunction(stateChangedFailed)) {
stateChangedFailed('invalidState', state);
}
return;
}
self.previousState = previousState = currentState;
self.currentState = currentState = state;
if (stateChanged && util.isFunction(stateChanged)) stateChanged(state, previousState);
}
};
// Returns a number of states and returns true if the current state
// is any of them.
//
// @example
// if (this.is('connecting', 'connected')) {
// // do some stuff
// }
//
self.is = function (/* state0:String, state1:String, ..., stateN:String */) {
return Array.prototype.indexOf.call(arguments, currentState) !== -1;
};
// Returns a number of states and returns true if the current state
// is none of them.
//
// @example
// if (this.isNot('connecting', 'connected')) {
// // do some stuff
// }
//
self.isNot = function (/* state0:String, state1:String, ..., stateN:String */) {
return Array.prototype.indexOf.call(arguments, currentState) === -1;
};
return setState;
};
},{"../util":34}],12:[function(require,module,exports){
'use strict';
var makeEverythingAttachToOTHelpers = require('./makeEverythingAttachToOTHelpers');
var callbacks = {};
module.exports = callbacks;
// Calls the function +fn+ asynchronously with the current execution.
// This is most commonly used to execute something straight after
// the current function.
//
// Any arguments in addition to +fn+ will be passed to +fn+ when it's
// called.
//
// You would use this inplace of setTimeout(fn, 0) type constructs. callAsync
// is preferable as it executes in a much more predictable time global,
// unlike setTimeout which could execute anywhere from 2ms to several thousand
// depending on the browser/context.
//
// It does this using global.postMessage, although if postMessage won't
// work it will fallback to setTimeout.
//
callbacks.callAsync = function (/* fn, [arg1, arg2, ..., argN] */) {
var args = Array.prototype.slice.call(arguments);
var fn = args.shift();
setTimeout(function() {
fn.apply(null, args);
}, 0);
};
// Wraps +handler+ in a function that will execute it asynchronously
// so that it doesn't interfere with it's exceution context if it raises
// an exception.
callbacks.createAsyncHandler = function(handler) {
return function() {
var args = Array.prototype.slice.call(arguments);
callbacks.callAsync(function() {
handler.apply(null, args);
});
};
};
makeEverythingAttachToOTHelpers(callbacks);
},{"./makeEverythingAttachToOTHelpers":31}],13:[function(require,module,exports){
'use strict';
var logging = require('./logging');
var makeEverythingAttachToOTHelpers = require('./makeEverythingAttachToOTHelpers');
var util = require('./util');
var capabilities = {};
module.exports = capabilities;
var capabilityRegistry = {};
var memoriseCapabilityTest;
// Registers a new capability type and a function that will indicate
// whether this client has that capability.
//
// OTHelpers.registerCapability('bundle', function() {
// return OTHelpers.hasCapabilities('webrtc') &&
// (OTHelpers.env.name === 'Chrome' || TBPlugin.isInstalled());
// });
//
capabilities.registerCapability = function(name, callback) {
var _name = name.toLowerCase();
if (capabilityRegistry.hasOwnProperty(_name)) {
logging.error('Attempted to register', name, 'capability more than once');
return;
}
if (!util.isFunction(callback)) {
logging.error('Attempted to register', name,
'capability with a callback that isn\' a function');
return;
}
memoriseCapabilityTest(_name, callback);
};
// Wrap up a capability test in a function that memorises the
// result.
memoriseCapabilityTest = function (name, callback) {
capabilityRegistry[name] = function() {
var result = callback();
capabilityRegistry[name] = function() {
return result;
};
return result;
};
};
var testCapability = function (name) {
return capabilityRegistry[name]();
};
/* test-code */
capabilities.__testOnly = {};
capabilities.__testOnly.disableCapabilityMemorisation = false;
var rawCapabilityRegistry = {},
defaultMemoriseCapabilityTest = memoriseCapabilityTest;
// Intercept the memorise call and store the raw callbacks in test mode
memoriseCapabilityTest = function (name, callback) {
rawCapabilityRegistry[name.toLowerCase()] = callback;
return defaultMemoriseCapabilityTest(name, callback);
};
// Override this call in test mode
testCapability = function (name) {
var cap;
if (capabilities.__testOnly.disableCapabilityMemorisation === true) {
cap = rawCapabilityRegistry[name];
}
else {
cap = capabilityRegistry[name];
}
return cap();
};
capabilities.__testOnly.resetCapabilities = function() {
rawCapabilityRegistry = {};
capabilityRegistry = {};
};
capabilities.__testOnly.unmemoriseCapability = function(name) {
memoriseCapabilityTest(name, rawCapabilityRegistry[name]);
};
capabilities.__testOnly.removeCapabilities = function() {
var names = Array.prototype.slice.call(arguments),
num = names.length;
for (var i = 0; i < num; ++i) {
delete rawCapabilityRegistry[names[i]];
delete capabilityRegistry[names[i]];
}
};
/* end-test-code */
// Returns true if all of the capability names passed in
// exist and are met.
//
// OTHelpers.hasCapabilities('bundle', 'rtcpMux')
//
capabilities.hasCapabilities = function(/* capability1, capability2, ..., capabilityN */) {
var capNames = Array.prototype.slice.call(arguments),
name;
for (var i = 0; i < capNames.length; ++i) {
name = capNames[i].toLowerCase();
if (!capabilityRegistry.hasOwnProperty(name)) {
logging.error('hasCapabilities was called with an unknown capability: ' + name);
return false;
}
else if (testCapability(name) === false) {
return false;
}
}
return true;
};
makeEverythingAttachToOTHelpers(capabilities);
},{"./logging":29,"./makeEverythingAttachToOTHelpers":31,"./util":34}],14:[function(require,module,exports){
'use strict';
var makeEverythingAttachToOTHelpers = require('./makeEverythingAttachToOTHelpers');
var casting = {};
module.exports = casting;
casting.castToBoolean = function(value, defaultValue) {
if (value === undefined) {
return defaultValue;
}
return value === 'true' || value === true;
};
casting.roundFloat = function(value, places) {
return Number(value.toFixed(places));
};
makeEverythingAttachToOTHelpers(casting);
},{"./makeEverythingAttachToOTHelpers":31}],15:[function(require,module,exports){
'use strict';
var eventing = require('./behaviours/eventing');
var logging = require('./logging');
var util = require('./util');
module.exports = function Collection(idField) {
var _models = [],
_byId = {},
_idField = idField || 'id';
eventing(this, true);
var modelProperty = function(model, property) {
if (util.isFunction(model[property])) {
return model[property]();
} else {
return model[property];
}
};
var onModelUpdate = function onModelUpdate(event) {
this.trigger('update', event);
this.trigger('update:'+event.target.id, event);
}.bind(this),
onModelDestroy = function onModelDestroyed(event) {
this.remove(event.target, event.reason);
}.bind(this);
this.reset = function() {
// Stop listening on the models, they are no longer our problem
_models.forEach(function(model) {
model.off('updated', onModelUpdate, this);
model.off('destroyed', onModelDestroy, this);
}, this);
_models = [];
_byId = {};
};
this.destroy = function(reason) {
_models.forEach(function(model) {
if(model && typeof model.destroy === 'function') {
model.destroy(reason, true);
}
});
this.reset();
this.off();
};
this.get = function(id) { return id && _byId[id] !== void 0 ? _models[_byId[id]] : void 0; };
this.has = function(id) { return id && _byId[id] !== void 0; };
this.toString = function() { return _models.toString(); };
// Return only models filtered by either a dict of properties
// or a filter function.
//
// @example Return all publishers with a streamId of 1
// OT.publishers.where({streamId: 1})
//
// @example The same thing but filtering using a filter function
// OT.publishers.where(function(publisher) {
// return publisher.stream.id === 4;
// });
//
// @example The same thing but filtering using a filter function
// executed with a specific this
// OT.publishers.where(function(publisher) {
// return publisher.stream.id === 4;
// }, self);
//
this.where = function(attrsOrFilterFn, context) {
if (util.isFunction(attrsOrFilterFn)) {
return _models.filter(attrsOrFilterFn, context);
}
return _models.filter(function(model) {
for (var key in attrsOrFilterFn) {
if(!attrsOrFilterFn.hasOwnProperty(key)) {
continue;
}
if (modelProperty(model, key) !== attrsOrFilterFn[key]) return false;
}
return true;
});
};
// Similar to where in behaviour, except that it only returns
// the first match.
this.find = function(attrsOrFilterFn, context) {
var filterFn;
if (util.isFunction(attrsOrFilterFn)) {
filterFn = attrsOrFilterFn;
}
else {
filterFn = function(model) {
for (var key in attrsOrFilterFn) {
if(!attrsOrFilterFn.hasOwnProperty(key)) {
continue;
}
if (modelProperty(model, key) !== attrsOrFilterFn[key]) return false;
}
return true;
};
}
filterFn = filterFn.bind(context);
for (var i=0; i<_models.length; ++i) {
if (filterFn(_models[i]) === true) return _models[i];
}
return null;
};
this.forEach = function(fn, context) {
_models.forEach(fn, context);
return this;
};
this.add = function(model) {
var id = modelProperty(model, _idField);
if (this.has(id)) {
logging.warn('Model ' + id + ' is already in the collection', _models);
return this;
}
_byId[id] = _models.push(model) - 1;
model.on('updated', onModelUpdate, this);
model.on('destroyed', onModelDestroy, this);
this.trigger('add', model);
this.trigger('add:'+id, model);
return this;
};
this.remove = function(model, reason) {
var id = modelProperty(model, _idField);
_models.splice(_byId[id], 1);
// Shuffle everyone down one
for (var i=_byId[id]; i<_models.length; ++i) {
_byId[_models[i][_idField]] = i;
}
delete _byId[id];
model.off('updated', onModelUpdate, this);
model.off('destroyed', onModelDestroy, this);
this.trigger('remove', model, reason);
this.trigger('remove:'+id, model, reason);
return this;
};
// Retrigger the add event behaviour for each model. You can also
// select a subset of models to trigger using the same arguments
// as the #where method.
this._triggerAddEvents = function() {
this.where.apply(this, arguments).forEach(function(model) {
this.trigger('add', model);
this.trigger('add:' + modelProperty(model, _idField), model);
}, this);
};
this.length = function() {
return _models.length;
};
};
},{"./behaviours/eventing":7,"./logging":29,"./util":34}],16:[function(require,module,exports){
'use strict';
var makeEverythingAttachToOTHelpers = require('./makeEverythingAttachToOTHelpers');
var cookies = {};
module.exports = cookies;
cookies.setCookie = function(key, value) {
try {
localStorage.setItem(key, value);
} catch (err) {
// Store in browser cookie
var date = new Date();
date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000));
var expires = '; expires=' + date.toGMTString();
document.cookie = key + '=' + value + expires + '; path=/';
}
};
cookies.getCookie = function(key) {
var value;
try {
value = localStorage.getItem(key);
return value;
} catch (err) {
// Check browser cookies
var nameEQ = key + '=';
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1,c.length);
}
if (c.indexOf(nameEQ) === 0) {
value = c.substring(nameEQ.length,c.length);
}
}
if (value) {
return value;
}
}
return null;
};
makeEverythingAttachToOTHelpers(cookies);
},{"./makeEverythingAttachToOTHelpers":31}],17:[function(require,module,exports){
'use strict';
var $ = require('./elementCollection/shorthandSelector');
var makeEverythingAttachToOTHelpers = require('./makeEverythingAttachToOTHelpers');
var domExtras = {};
module.exports = domExtras;
domExtras.isElementNode = function(node) {
return node && typeof node === 'object' && node.nodeType === 1;
};
domExtras.createElement = function(nodeName, attributes, children, doc) {
var element = (doc || document).createElement(nodeName);
if (attributes) {
for (var name in attributes) {
if (typeof(attributes[name]) === 'object') {
if (!element[name]) element[name] = {};
var subAttrs = attributes[name];
for (var n in subAttrs) {
element[name][n] = subAttrs[n];
}
}
else if (name === 'className') {
element.className = attributes[name];
}
else {
element.setAttribute(name, attributes[name]);
}
}
}
var setChildren = function(child) {
if(typeof child === 'string') {
element.innerHTML = element.innerHTML + child;
} else {
element.appendChild(child);
}
};
if (Array.isArray(children)) {
children.forEach(setChildren);
} else if(children) {
setChildren(children);
}
return element;
};
domExtras.createButton = function(innerHTML, attributes, events) {
var button = domExtras.createElement('button', attributes, innerHTML);
if (events) {
for (var name in events) {
if (events.hasOwnProperty(name)) {
$(button).on(name, events[name]);
}
}
button._boundEvents = events;
}
return button;
};
makeEverythingAttachToOTHelpers(domExtras);
},{"./elementCollection/shorthandSelector":24,"./makeEverythingAttachToOTHelpers":31}],18:[function(require,module,exports){
(function (global){
'use strict';
var makeEverythingAttachToOTHelpers = require('./makeEverythingAttachToOTHelpers');
var domLoad = {};
module.exports = domLoad;
var _domReady = typeof(document) === 'undefined' ||
document.readyState === 'complete' ||
(document.readyState === 'interactive' && document.body),
_loadCallbacks = [],
_unloadCallbacks = [],
_domUnloaded = false,
onDomUnload = function() {
_domUnloaded = true;
_unloadCallbacks.forEach(function(listener) {
listener[0].call(listener[1]);
});
_unloadCallbacks = [];
},
onDomReady = function() {
_domReady = true;
if (typeof(document) !== 'undefined') {
document.removeEventListener('DOMContentLoaded', onDomReady, false);
global.removeEventListener('load', onDomReady, false);
// This is making an assumption about there being only one 'global'
// that we care about.
var globalonunload = global.onunload;
global.onunload = function() {
if (typeof globalonunload === 'function') {
globalonunload.apply(undefined, arguments);
}
onDomUnload();
}
}
// @TODO consider how to trigger/handle clean up Node. This is
// obviously not the Object to do it in, as there is no DOM in
// Node. But this is where the equivilant browser code is so
// it's a good place to leave a todo.
_loadCallbacks.forEach(function(listener) {
listener[0].call(listener[1]);
});
_loadCallbacks = [];
};
domLoad.isReady = function() {
return !_domUnloaded && _domReady;
};
domLoad.onDOMLoad = function(cb, context) {
if (domLoad.isReady()) {
cb.call(context);
return;
}
_loadCallbacks.push([cb, context]);
};
domLoad.onDOMUnload = function(cb, context) {
if (this.isDOMUnloaded()) {
cb.call(context);
return;
}
_unloadCallbacks.push([cb, context]);
};
domLoad.isDOMUnloaded = function() {
return _domUnloaded;
};
if (_domReady) {
onDomReady();
} else if(typeof(document) !== 'undefined') {
document.addEventListener('DOMContentLoaded', onDomReady, false);
// fallback
global.addEventListener('load', onDomReady, false );
}
makeEverythingAttachToOTHelpers(domLoad);
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./makeEverythingAttachToOTHelpers":31}],19:[function(require,module,exports){
'use strict';
var util = require('../../util');
var specialDomProperties = {
'for': 'htmlFor',
'class': 'className'
};
module.exports = function(ElementCollection) {
// Gets or sets the attribute called +name+ for the first element in the collection
ElementCollection.prototype.attr = function (name, value) {
if (util.isObject(name)) {
var actualName;
for (var key in name) {
actualName = specialDomProperties[key] || key;
this.first.setAttribute(actualName, name[key]);
}
}
else if (value === void 0) {
return this.first.getAttribute(specialDomProperties[name] || name);
}
else {
this.first.setAttribute(specialDomProperties[name] || name, value);
}
return this;
};
// Removes an attribute called +name+ for the every element in the collection.
ElementCollection.prototype.removeAttr = function (name) {
var actualName = specialDomProperties[name] || name;
this.forEach(function(element) {
element.removeAttribute(actualName);
});
return this;
};
// Gets, and optionally sets, the html body of the first element
// in the collection. If the +html+ is provided then the first
// element's html body will be replaced with it.
//
ElementCollection.prototype.html = function (html) {
if (html !== void 0) {
this.first.innerHTML = html;
}
return this.first.innerHTML;
};
// Centers +element+ within the global. You can pass through the width and height
// if you know it, if you don't they will be calculated for you.
ElementCollection.prototype.center = function (width, height) {
var $element;
this.forEach(function(element) {
$element = new ElementCollection(element);
if (!width) width = parseInt($element.width(), 10);
if (!height) height = parseInt($element.height(), 10);
var marginLeft = -0.5 * width + 'px';
var marginTop = -0.5 * height + 'px';
$element.css('margin', marginTop + ' 0 0 ' + marginLeft)
.addClass('OT_centered');
});
return this;
};
// @remove
// Centers +element+ within the global. You can pass through the width and height
// if you know it, if you don't they will be calculated for you.
ElementCollection._attachToOTHelpers.centerElement = function(element, width, height) {
return new ElementCollection(element).center(width, height);
};
/**
* Methods to calculate element widths and heights.
*/
var _width = function(element) {
if (element.offsetWidth > 0) {
return element.offsetWidth + 'px';
}
return new ElementCollection(element).css('width');
},
_height = function(element) {
if (element.offsetHeight > 0) {
return element.offsetHeight + 'px';
}
return new ElementCollection(element).css('height');
};
ElementCollection.prototype.width = function (newWidth) {
if (newWidth) {
this.css('width', newWidth);
return this;
}
else {
if (this.isDisplayNone()) {
return this.makeVisibleAndYield(function(element) {
return _width(element);
})[0];
}
else {
return _width(this.get(0));
}
}
};
// @remove
ElementCollection._attachToOTHelpers.width = function(element, newWidth) {
var ret = new ElementCollection(element).width(newWidth);
return newWidth ? ElementCollection._attachToOTHelpers : ret;
};
ElementCollection.prototype.height = function (newHeight) {
if (newHeight) {
this.css('height', newHeight);
return this;
}
else {
if (this.isDisplayNone()) {
// We can't get the height, probably since the element is hidden.
return this.makeVisibleAndYield(function(element) {
return _height(element);
})[0];
}
else {
return _height(this.get(0));
}
}
};
// @remove
ElementCollection._attachToOTHelpers.height = function(element, newHeight) {
var ret = new ElementCollection(element).height(newHeight);
return newHeight ? ElementCollection._attachToOTHelpers : ret;
};
};
},{"../../util":34}],20:[function(require,module,exports){
'use strict';
var capabilities = require('../../capabilities');
function isClassListSupported() {
return (typeof document !== 'undefined') && ('classList' in document.createElement('a'));
}
var classListSupported = isClassListSupported();
// Returns true if the client supports element.classList
capabilities.registerCapability('classList', isClassListSupported);
function hasClass(element, className) {
if (!className) {
return false;
}
if (classListSupported) {
return element.classList.contains(className);
}
return element.className.indexOf(className) > -1;
}
function toggleClasses(element, classNames) {
if (!classNames || classNames.length === 0) return;
// Only bother targeting Element nodes, ignore Text Nodes, CDATA, etc
if (element.nodeType !== 1) {
return;
}
var numClasses = classNames.length,
i = 0;
if (classListSupported) {
for (; iRTCPeerConnection.getStats based audio level sampler.
*
* It uses the the getStats method to get the audioOutputLevel.
* This implementation expects the single parameter version of the getStats method.
*
* Currently the audioOutputLevel stats is only supported in Chrome.
*
* @param {function} getStatsFn the "getStats" function
* @constructor
*/
module.exports = function GetstatsAudioOutputLevelSampler(getStatsFn) {
/*
* Acquires the audio level.
*
* @param {function(?number)} done a callback to be called with the acquired value in the
* [0, 1] range when available or null if no value could be acquired
*/
this.sample = function(done) {
getStatsFn(function(error, stats) {
if (!error) {
for (var idx = 0; idx < stats.length; idx++) {
var stat = stats[idx];
var audioOutputLevel = parseFloat(stat.audioOutputLevel);
if (!isNaN(audioOutputLevel)) {
// the mex value delivered by getStats for audio levels is 2^15
done(audioOutputLevel / 32768);
return;
}
}
}
done(null);
});
};
};
},{}],143:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
/*
* An AudioContext based audio level sampler. It returns the maximum value in the
* last 1024 samples.
*
* It is worth noting that the remote MediaStream audio analysis is currently only
* available in FF.
*
* This implementation gracefully handles the case where the MediaStream has not
* been set yet by returning a null value until the stream is set. It is up to the
* call site to decide what to do with this value (most likely ignore it and retry later).
*
* @constructor
* @param {AudioContext} audioContext an audio context instance to get an analyser node
*/
module.exports = function WebaudioAudioLevelSampler(audioContext) {
var _sampler = this;
var _analyser = null;
var _timeDomainData = null;
var _webRTCStream = null;
var buildAnalyzer = function(stream) {
var sourceNode = audioContext.createMediaStreamSource(stream);
var analyser = audioContext.createAnalyser();
sourceNode.connect(analyser);
return analyser;
};
OTHelpers.defineProperties(_sampler, {
webRTCStream: {
get: function() {
return _webRTCStream;
},
set: function(webRTCStream) {
// when the stream is updated we need to create a new analyzer
_webRTCStream = webRTCStream;
_analyser = buildAnalyzer(_webRTCStream);
_timeDomainData = new Uint8Array(_analyser.frequencyBinCount);
}
}
});
this.sample = function(done) {
if (_analyser) {
_analyser.getByteTimeDomainData(_timeDomainData);
// varies from 0 to 255
var max = 0;
for (var idx = 0; idx < _timeDomainData.length; idx++) {
max = Math.max(max, Math.abs(_timeDomainData[idx] - 128));
}
// normalize the collected level to match the range delivered by
// the getStats' audioOutputLevel
done(max / 128);
} else {
done(null);
}
};
};
},{"@opentok/ot-helpers":4}],144:[function(require,module,exports){
(function (global){
'use strict';
var logging = require('../ot/logging.js');
var properties = require('./properties.js');
var OTPlugin = require('@opentok/otplugin.js');
var OTHelpers = require('@opentok/ot-helpers');
///
// Capabilities
//
// Support functions to query browser/client Media capabilities.
//
// Indicates whether this client supports the getUserMedia
// API.
//
OTHelpers.registerCapability('getUserMedia', function() {
if (OTHelpers.env.name === 'Node') { return false; }
return !!(global.navigator.webkitGetUserMedia ||
global.navigator.mozGetUserMedia ||
(global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia) ||
OTPlugin.isInstalled());
});
// TODO Remove all PeerConnection stuff, that belongs to the messaging layer not the Media layer.
// Indicates whether this client supports the PeerConnection
// API.
//
// Chrome Issues:
// * The explicit prototype.addStream check is because webkitRTCPeerConnection was
// partially implemented, but not functional, in Chrome 22.
//
// Firefox Issues:
// * No real support before Firefox 19
// * Firefox 19 has issues with generating Offers.
// * Firefox 20 doesn't interoperate with Chrome.
//
OTHelpers.registerCapability('PeerConnection', function() {
if (OTHelpers.env === 'Node') {
return false;
} else if (typeof global.webkitRTCPeerConnection === 'function' &&
!!global.webkitRTCPeerConnection.prototype.addStream) {
return true;
} else if (typeof global.RTCPeerConnection === 'function') {
return true;
} else if (typeof global.mozRTCPeerConnection === 'function' && OTHelpers.env.version > 20.0) {
return true;
}
return OTPlugin.isInstalled();
});
// Indicates whether this client supports WebRTC
//
// This is defined as: getUserMedia + PeerConnection + exceeds min browser version
//
OTHelpers.registerCapability('webrtc', function() {
if (properties) {
var minimumVersions = properties.minimumVersion || {};
var minimumVersion = minimumVersions[OTHelpers.env.name.toLowerCase()];
if (minimumVersion && OTHelpers.env.versionGreaterThan(minimumVersion)) {
logging.debug('Support for', OTHelpers.env.name, 'is disabled because we require',
minimumVersion, 'but this is', OTHelpers.env.version);
return false;
}
}
if (OTHelpers.env === 'Node') {
// Node works, even though it doesn't have getUserMedia
return true;
}
return OTHelpers.hasCapabilities('getUserMedia', 'PeerConnection');
});
// TODO Remove all transport stuff, that belongs to the messaging layer not the Media layer.
// Indicates if the browser supports bundle
//
// Broadly:
// * Firefox doesn't support bundle
// * Chrome support bundle
// * OT Plugin supports bundle
// * We assume NodeJs supports bundle (e.g. 'you're on your own' mode)
//
OTHelpers.registerCapability('bundle', function() {
return OTHelpers.hasCapabilities('webrtc') &&
(OTHelpers.env.name === 'Chrome' ||
OTHelpers.env.name === 'Node' ||
OTPlugin.isInstalled());
});
// Indicates if the browser supports RTCP Mux
//
// Broadly:
// * Older versions of Firefox (<= 25) don't support RTCP Mux
// * Older versions of Firefox (>= 26) support RTCP Mux (not tested yet)
// * Chrome support RTCP Mux
// * OT Plugin supports RTCP Mux
// * We assume NodeJs supports RTCP Mux (e.g. 'you're on your own' mode)
//
OTHelpers.registerCapability('RTCPMux', function() {
return OTHelpers.hasCapabilities('webrtc') &&
(OTHelpers.env.name === 'Chrome' ||
OTHelpers.env.name === 'Node' ||
OTPlugin.isInstalled());
});
OTHelpers.registerCapability('audioOutputLevelStat', function() {
return ['Chrome', 'IE', 'Opera'].indexOf(OTHelpers.env.name) > -1;
});
OTHelpers.registerCapability('webAudioCapableRemoteStream', function() {
return OTHelpers.env.name === 'Firefox';
});
OTHelpers.registerCapability('webAudio', function() {
return 'AudioContext' in global;
});
OTHelpers.registerCapability('iceRestarts', function() {
return OTHelpers.hasCapabilities('webrtc') &&
OTHelpers.env.name === 'Chrome';
});
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"../ot/logging.js":187,"./properties.js":154,"@opentok/ot-helpers":4,"@opentok/otplugin.js":40}],145:[function(require,module,exports){
'use strict';
// TODO: this is not unit tested
var analytics = require('../ot/analytics.js');
var OTHelpers = require('@opentok/ot-helpers');
module.exports = function ConnectivityAttemptPinger(options) {
var _state = 'Initial';
var states = ['Initial', 'Attempt', 'Success', 'Failure'];
var PING_INTERVAL = 5000;
var PING_COUNT_TOTAL = 6;
var _previousState,
pingTimer, // Timer for the Attempting ping;
pingCount;
//// Private API
var stateChanged = function(newState) {
_state = newState;
var invalidSequence = false;
switch (_state) {
case 'Attempt':
if (_previousState !== 'Initial') {
invalidSequence = true;
}
startAttemptPings();
break;
case 'Success':
if (_previousState !== 'Attempt') {
invalidSequence = true;
}
stopAttemptPings();
break;
case 'Failure':
if (_previousState !== 'Attempt') {
invalidSequence = true;
}
stopAttemptPings();
break;
default:
}
if (invalidSequence) {
var data = options ? OTHelpers.clone(options) : {};
data.action = 'Internal Error';
data.variation = 'Non-fatal';
data.payload = {
debug: 'Invalid sequence: ' + options.action + ' ' +
_previousState + ' -> ' + _state
};
analytics.logEvent(data);
}
};
var setState = OTHelpers.statable(this, states, 'Failure', stateChanged);
var startAttemptPings = function() {
pingCount = 0;
pingTimer = setInterval(function() {
if (pingCount < PING_COUNT_TOTAL) {
var data = OTHelpers.extend(options, { variation: 'Attempting' });
analytics.logEvent(data);
} else {
stopAttemptPings();
}
pingCount++;
}, PING_INTERVAL);
};
var stopAttemptPings = function() {
clearInterval(pingTimer);
};
this.setVariation = function(variation) {
_previousState = _state;
setState(variation);
// We could change the ConnectivityAttemptPinger to a ConnectivityAttemptLogger
// that also logs events in addition to logging the ping ('Attempting') events.
//
// var payload = OTHelpers.extend(options, {variation:variation});
// analytics.logEvent(payload);
};
this.stop = function() {
stopAttemptPings();
};
};
},{"../ot/analytics.js":166,"@opentok/ot-helpers":4}],146:[function(require,module,exports){
(function (global){
'use strict';
var OTPlugin = require('@opentok/otplugin.js');
var NativeRTCPeerConnection = (global.webkitRTCPeerConnection ||
global.RTCPeerConnection ||
global.mozRTCPeerConnection);
module.exports = function createPeerConnection(
config,
options,
publishersWebRtcStream,
completion
) {
if (OTPlugin.isInstalled()) {
OTPlugin.initPeerConnection(config, options,
publishersWebRtcStream, completion);
} else {
var pc;
try {
pc = new NativeRTCPeerConnection(config, options);
} catch (e) {
completion(e.message);
return;
}
completion(null, pc);
}
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"@opentok/otplugin.js":40}],147:[function(require,module,exports){
'use strict';
module.exports = function cssLoader(cssURL) {
var style = document.createElement('link');
style.type = 'text/css';
style.media = 'screen';
style.rel = 'stylesheet';
style.href = cssURL;
var head = document.head || document.getElementsByTagName('head')[0];
head.appendChild(style);
};
},{}],148:[function(require,module,exports){
(function (global){
'use strict';
// Web OT Helpers
var OTPlugin = require('@opentok/otplugin.js');
var OTHelpers = require('@opentok/ot-helpers');
var deviceHelpers = {};
module.exports = deviceHelpers;
///
// Device Helpers
//
// Support functions to enumerating and guerying device info
//
var deviceKindsMap = {
audio: 'audioInput',
video: 'videoInput',
audioinput: 'audioInput',
videoinput: 'videoInput',
audioInput: 'audioInput',
videoInput: 'videoInput'
};
var getNativeEnumerateDevices = function() {
if (global.navigator.mediaDevices) {
return global.navigator.mediaDevices.enumerateDevices;
} else if (OTHelpers.hasCapabilities('otplugin')) {
return OTPlugin.mediaDevices.enumerateDevices;
} else if (global.MediaStreamTrack && global.MediaStreamTrack.getSources) {
return global.MediaStreamTrack.getSources;
}
};
var enumerateDevices = function(completion) {
var fn = getNativeEnumerateDevices();
if (global.navigator.mediaDevices && OTHelpers.isFunction(fn)) {
// De-promisify the newer style APIs. We aren't ready for Promises yet...
fn = function(completion) {
global.navigator.mediaDevices.enumerateDevices().then(function(devices) {
completion(void 0, devices);
})['catch'](function(err) {
completion(err);
});
};
} else if (global.MediaStreamTrack && global.MediaStreamTrack.getSources) {
fn = function(completion) {
global.MediaStreamTrack.getSources(function(devices) {
completion(void 0, devices);
});
};
}
return fn(completion);
};
var fakeShouldAskForDevices = function fakeShouldAskForDevices(callback) {
setTimeout(callback.bind(null, { video: true, audio: true }));
};
// Indicates whether this browser supports the enumerateDevices (getSources) API.
//
OTHelpers.registerCapability('enumerateDevices', function() {
return OTHelpers.isFunction(getNativeEnumerateDevices());
});
deviceHelpers.getMediaDevices = function(completion, customGetDevices) {
if (!OTHelpers.hasCapabilities('enumerateDevices')) {
completion(new Error('This browser does not support enumerateDevices APIs'));
return;
}
var getDevices = OTHelpers.isFunction(customGetDevices) ? customGetDevices
: enumerateDevices;
getDevices(function(err, devices) {
if (err) {
completion(err);
return;
}
// Normalise the device kinds
var filteredDevices = OTHelpers(devices).map(function(device) {
return {
deviceId: device.deviceId || device.id,
label: device.label,
kind: deviceKindsMap[device.kind]
};
}).filter(function(device) {
return device.kind === 'audioInput' || device.kind === 'videoInput';
});
completion(void 0, filteredDevices.get());
});
};
deviceHelpers.shouldAskForDevices = function(callback) {
if (OTHelpers.hasCapabilities('enumerateDevices')) {
deviceHelpers.getMediaDevices(function(err, devices) {
if (err) {
// There was an error. It may be temporally. Just assume
// all devices exist for now.
fakeShouldAskForDevices(callback);
return;
}
var audioDevices = devices.filter(function(device) {
return device.kind === 'audioInput';
});
var videoDevices = devices.filter(function(device) {
return device.kind === 'videoInput';
});
callback.call(null, {
video: videoDevices.length > 0,
audio: audioDevices.length > 0,
videoDevices: videoDevices,
audioDevices: audioDevices
});
});
} else {
// This environment can't enumerate devices anyway, so we'll memorise this result.
// TODO: Is memoization worth it here?
deviceHelpers.shouldAskForDevices = fakeShouldAskForDevices;
deviceHelpers.shouldAskForDevices(callback);
}
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"@opentok/ot-helpers":4,"@opentok/otplugin.js":40}],149:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
var properties = require('./properties.js');
var logging = require('../ot/logging.js');
var Dialogs = {};
module.exports = Dialogs;
var addCss = function(document, url, callback) {
var head = document.head || document.getElementsByTagName('head')[0];
var cssTag = OTHelpers.createElement('link', {
type: 'text/css',
media: 'screen',
rel: 'stylesheet',
href: url
});
head.appendChild(cssTag);
OTHelpers.on(cssTag, 'error', function(error) {
logging.error('Could not load CSS for dialog', url, error && error.message || error);
});
OTHelpers.on(cssTag, 'load', callback);
};
var addDialogCSS = function(document, urls, callback) {
var allURLs = [
'//fonts.googleapis.com/css?family=Didact+Gothic',
properties.cssURL
].concat(urls);
var remainingStylesheets = allURLs.length;
allURLs.forEach(function(stylesheetUrl) {
addCss(document, stylesheetUrl, function() {
if (--remainingStylesheets <= 0) {
callback();
}
});
});
};
var templateElement = function(classes, children, tagName) {
var el = OTHelpers.createElement(tagName || 'div', { class: classes }, children, this);
el.on = OTHelpers.on.bind(OTHelpers, el);
el.off = OTHelpers.off.bind(OTHelpers, el);
return el;
};
var checkBoxElement = function(classes, nameAndId, onChange) {
var checkbox = templateElement.call(this, '', null, 'input');
checkbox = OTHelpers(checkbox).on('change', onChange);
checkbox.attr({
name: nameAndId,
id: nameAndId,
type: 'checkbox'
});
return checkbox.first;
};
var linkElement = function(children, href, classes) {
var link = templateElement.call(this, classes || '', children, 'a');
link.setAttribute('href', href);
return link;
};
Dialogs.Plugin = {};
Dialogs.Plugin.promptToInstall = function() {
var modal = new OTHelpers.Modal(function(global, document) {
var el = templateElement.bind(document);
var btn = function(children, size) {
var classes = 'OT_dialog-button ' +
(size ? 'OT_dialog-button-' + size : 'OT_dialog-button-large');
var b = el(classes, children);
b.enable = function() {
OTHelpers.removeClass(this, 'OT_dialog-button-disabled');
return this;
};
b.disable = function() {
OTHelpers.addClass(this, 'OT_dialog-button-disabled');
return this;
};
return b;
};
var downloadButton = btn('Download plugin');
var cancelButton = btn('cancel', 'small');
var refreshButton = btn('Refresh browser');
var acceptEULA, checkbox, close, root;
OTHelpers.addClass(cancelButton, 'OT_dialog-no-natural-margin OT_dialog-button-block');
OTHelpers.addClass(refreshButton, 'OT_dialog-no-natural-margin');
function onDownload() {
modal.trigger('download');
setTimeout(function() {
root.querySelector('.OT_dialog-messages-main').innerHTML
= 'To enable audio & video chat on this website, run the installer and then refresh ' +
'your browser window.';
var sections = root.querySelectorAll('.OT_dialog-section');
OTHelpers.addClass(sections[0], 'OT_dialog-hidden');
OTHelpers.removeClass(sections[1], 'OT_dialog-hidden');
}, 3000);
}
function onRefresh() {
modal.trigger('refresh');
}
function onToggleEULA() {
if (checkbox.checked) {
enableButtons();
} else {
disableButtons();
}
}
function enableButtons() {
downloadButton.enable();
downloadButton.on('click', onDownload);
refreshButton.enable();
refreshButton.on('click', onRefresh);
}
function disableButtons() {
downloadButton.disable();
downloadButton.off('click', onDownload);
refreshButton.disable();
refreshButton.off('click', onRefresh);
}
downloadButton.disable();
refreshButton.disable();
cancelButton.on('click', function() {
modal.trigger('cancelButtonClicked');
modal.close();
});
close = el('OT_closeButton', '×')
.on('click', function() {
modal.trigger('closeButtonClicked');
modal.close();
}).first;
var protocol = (global.location.protocol.indexOf('https') >= 0 ? 'https' : 'http');
acceptEULA = linkElement.call(document,
'end-user license agreement',
protocol + '://tokbox.com/support/ie-eula');
checkbox = checkBoxElement.call(document, null, 'acceptEULA', onToggleEULA);
root = el('OT_dialog-centering', [
el('OT_dialog-centering-child', [
el('OT_root OT_dialog OT_dialog-plugin-prompt', [
close,
el('OT_dialog-messages', [
el('OT_dialog-messages-main', 'This app requires real-time communication')
]),
el('OT_dialog-section', [
el('OT_dialog-single-button-with-title', [
el('OT_dialog-button-title', [
checkbox,
(function() {
var x = el('', 'accept', 'label');
x.setAttribute('for', checkbox.id);
x.style.margin = '0 5px';
return x;
})(),
acceptEULA
]),
el('OT_dialog-actions-card', [
downloadButton,
cancelButton
])
])
]),
el('OT_dialog-section OT_dialog-hidden', [
el('OT_dialog-button-title', [
'You will then be able to continue using this application in Internet Explorer.'
]),
refreshButton
])
])
])
]);
addDialogCSS(document, [], function() {
document.body.appendChild(root);
});
});
return modal;
};
Dialogs.Plugin.promptToReinstall = function() {
var modal = new OTHelpers.Modal(function(global, document) {
var el = templateElement.bind(document);
var close, okayButton, root;
close = el('OT_closeButton', '×')
.on('click', function() {
modal.trigger('closeButtonClicked');
modal.close();
}).first;
okayButton =
el('OT_dialog-button OT_dialog-button-large OT_dialog-no-natural-margin', 'Okay')
.on('click', function() {
modal.trigger('okay');
}).first;
root = el('OT_dialog-centering', [
el('OT_dialog-centering-child', [
el('OT_ROOT OT_dialog OT_dialog-plugin-reinstall', [
close,
el('OT_dialog-messages', [
el('OT_dialog-messages-main', 'Reinstall Opentok Plugin'),
el('OT_dialog-messages-minor', 'Uh oh! Try reinstalling the OpenTok plugin again ' +
'to enable real-time video communication for Internet Explorer.')
]),
el('OT_dialog-section', [
el('OT_dialog-single-button', okayButton)
])
])
])
]);
addDialogCSS(document, [], function() {
document.body.appendChild(root);
});
});
return modal;
};
Dialogs.Plugin.updateInProgress = function() {
var progressValue = 0;
var progressBar, progressText;
var modal = new OTHelpers.Modal(function(global, document) {
var el = templateElement.bind(document);
var root;
progressText = el('OT_dialog-plugin-upgrade-percentage', '0%', 'strong');
progressBar = el('OT_dialog-progress-bar-fill');
root = el('OT_dialog-centering', [
el('OT_dialog-centering-child', [
el('OT_ROOT OT_dialog OT_dialog-plugin-upgrading', [
el('OT_dialog-messages', [
el('OT_dialog-messages-main', [
'One moment please... ',
progressText
]),
el('OT_dialog-progress-bar', progressBar),
el('OT_dialog-messages-minor OT_dialog-no-natural-margin',
'Please wait while the OpenTok plugin is updated')
])
])
])
]);
addDialogCSS(document, [], function() {
document.body.appendChild(root);
if (progressValue != null) {
modal.setUpdateProgress(progressValue);
}
});
});
modal.setUpdateProgress = function(newProgress) {
if (progressBar && progressText) {
if (newProgress > 99) {
OTHelpers.css(progressBar, 'width', '');
progressText.innerHTML = '100%';
} else if (newProgress < 1) {
OTHelpers.css(progressBar, 'width', '0%');
progressText.innerHTML = '0%';
} else {
OTHelpers.css(progressBar, 'width', newProgress + '%');
progressText.innerHTML = newProgress + '%';
}
} else {
progressValue = newProgress;
}
};
return modal;
};
Dialogs.Plugin.updateComplete = function(error) {
var modal = new OTHelpers.Modal(function(global, document) {
var el = templateElement.bind(document);
var reloadButton, root, msgs;
reloadButton =
el('OT_dialog-button OT_dialog-button-large OT_dialog-no-natural-margin', 'Reload')
.on('click', function() {
modal.trigger('reload');
}).first;
if (error) {
msgs = ['Update Failed.', error + '' || 'NO ERROR'];
} else {
msgs = ['Update Complete.',
'The OpenTok plugin has been succesfully updated. ' +
'Please reload your browser.'];
}
root = el('OT_dialog-centering', [
el('OT_dialog-centering-child', [
el('OT_root OT_dialog OT_dialog-plugin-upgraded', [
el('OT_dialog-messages', [
el('OT_dialog-messages-main', msgs[0]),
el('OT_dialog-messages-minor', msgs[1])
]),
el('OT_dialog-single-button', reloadButton)
])
])
]);
addDialogCSS(document, [], function() {
document.body.appendChild(root);
});
});
return modal;
};
},{"../ot/logging.js":187,"./properties.js":154,"@opentok/ot-helpers":4}],150:[function(require,module,exports){
(function (global){
'use strict';
var Bluebird = require('bluebird');
// Web OT Helpers
//
var OTPlugin = require('@opentok/otplugin.js');
var OTHelpers = require('@opentok/ot-helpers');
var logging = require('../ot/logging.js');
var throttleUntilComplete = require('./throttle_until_complete.js');
var nativeGetUserMedia,
getUserMedia,
throttledGetUserMedia,
vendorToW3CErrors,
gumNamesToMessages,
mapVendorErrorName,
parseErrorEvent;
// Handy cross-browser getUserMedia shim. Inspired by some code from Adam Barth
nativeGetUserMedia = (function() {
if (global.navigator.getUserMedia) {
return global.navigator.getUserMedia.bind(global.navigator);
} else if (global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia) {
return function(constraints, onStream, onError) {
global.navigator.mediaDevices.getUserMedia(constraints).then(onStream, onError);
};
} else if (global.navigator.mozGetUserMedia) {
return global.navigator.mozGetUserMedia.bind(global.navigator);
} else if (global.navigator.webkitGetUserMedia) {
return global.navigator.webkitGetUserMedia.bind(global.navigator);
} else if (OTPlugin.isInstalled()) {
return OTPlugin.getUserMedia.bind(OTPlugin);
}
})();
// Convert nativeGetUserMedia to use promises
var promisifiedGetUserMedia = function(constraints) {
return new Bluebird.Promise(function(resolve, reject) {
nativeGetUserMedia(constraints, resolve, reject);
});
};
// This waits to call getUserMedia() until after the access allowed db is closed
throttledGetUserMedia = throttleUntilComplete(promisifiedGetUserMedia);
// Mozilla error strings and the equivalent W3C names. NOT_SUPPORTED_ERROR does not
// exist in the spec right now, so we'll include Mozilla's error description.
// Chrome TrackStartError is triggered when the camera is already used by another app (Windows)
vendorToW3CErrors = {
PERMISSION_DENIED: 'PermissionDeniedError',
SecurityError: 'PermissionDeniedError',
NOT_SUPPORTED_ERROR: 'NotSupportedError',
MANDATORY_UNSATISFIED_ERROR: ' ConstraintNotSatisfiedError',
NO_DEVICES_FOUND: 'NoDevicesFoundError',
HARDWARE_UNAVAILABLE: 'HardwareUnavailableError',
TrackStartError: 'HardwareUnavailableError'
};
gumNamesToMessages = {
PermissionDeniedError: 'End-user denied permission to hardware devices',
PermissionDismissedError: 'End-user dismissed permission to hardware devices',
NotSupportedError: 'A constraint specified is not supported by the browser.',
ConstraintNotSatisfiedError: 'It\'s not possible to satisfy one or more constraints ' +
'passed into the getUserMedia function',
OverconstrainedError: 'Due to changes in the environment, one or more mandatory ' +
'constraints can no longer be satisfied.',
NoDevicesFoundError: 'No voice or video input devices are available on this machine.',
HardwareUnavailableError: 'The selected voice or video devices are unavailable. Verify ' +
'that the chosen devices are not in use by another application.'
};
// Map vendor error strings to names and messages if possible
mapVendorErrorName = function mapVendorErrorName(vendorErrorName, vendorErrors) {
var errorName, errorMessage;
if (vendorErrors.hasOwnProperty(vendorErrorName)) {
errorName = vendorErrors[vendorErrorName];
} else {
// This doesn't map to a known error from the Media Capture spec, it's
// probably a custom vendor error message.
errorName = vendorErrorName;
}
if (gumNamesToMessages.hasOwnProperty(errorName)) {
errorMessage = gumNamesToMessages[errorName];
} else {
errorMessage = 'Unknown Error while getting user media';
}
return {
name: errorName,
message: errorMessage
};
};
// Parse and normalise a getUserMedia error event from Chrome or Mozilla
// @ref http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-NavigatorUserMediaError
parseErrorEvent = function parseErrorObject(event) {
var error;
if (OTHelpers.isObject(event) && event.name) {
error = mapVendorErrorName(event.name, vendorToW3CErrors);
error.constraintName = event.constraintName;
} else if (typeof event === 'string') {
error = mapVendorErrorName(event, vendorToW3CErrors);
} else {
error = {
message: 'Unknown Error type while getting media'
};
}
return error;
};
// Validates a Hash of getUserMedia constraints. Currently we only
// check to see if there is at least one non-false constraint.
var areInvalidConstraints = function(constraints) {
if (!constraints || !OTHelpers.isObject(constraints)) { return true; }
for (var key in constraints) {
if (!constraints.hasOwnProperty(key)) {
continue;
}
if (constraints[key]) { return false; }
}
return true;
};
// A wrapper for the builtin navigator.getUserMedia. In addition to the usual
// getUserMedia behaviour, this helper method also accepts a accessDialogOpened
// and accessDialogClosed callback.
//
// @memberof OTHelpers
// @private
//
// @param {Object} constraints
// A dictionary of constraints to pass to getUserMedia. See
// http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-MediaStreamConstraints
// in the Media Capture and Streams spec for more info.
//
// @param {function} success
// Called when getUserMedia completes successfully. The callback will be passed a WebRTC
// Stream object.
//
// @param {function} failure
// Called when getUserMedia fails to access a user stream. It will be passed an object
// with a code property representing the error that occurred.
//
// @param {function} accessDialogOpened
// Called when the access allow/deny dialog is opened.
//
// @param {function} accessDialogClosed
// Called when the access allow/deny dialog is closed.
//
// @param {function} accessDenied
// Called when access is denied to the camera/mic. This will be either because
// the user has clicked deny or because a particular origin is permanently denied.
//
module.exports = function(constraints, success, failure, accessDialogOpened,
accessDialogClosed, accessDenied, customGetUserMedia) {
getUserMedia = throttledGetUserMedia;
if (OTHelpers.isFunction(customGetUserMedia)) {
getUserMedia = customGetUserMedia;
}
// All constraints are false, we don't allow this. This may be valid later
// depending on how/if we integrate data channels.
if (areInvalidConstraints(constraints)) {
logging.error('Couldn\'t get UserMedia: All constraints were false');
// Using a ugly dummy-code for now.
failure.call(null, {
name: 'NO_VALID_CONSTRAINTS',
message: 'Video and Audio was disabled, you need to enable at least one'
});
return;
}
var triggerOpenedTimer = null;
var displayedPermissionDialog = false;
var finaliseAccessDialog = function() {
if (triggerOpenedTimer) {
clearTimeout(triggerOpenedTimer);
}
if (displayedPermissionDialog && accessDialogClosed) { accessDialogClosed(); }
};
var triggerOpened = function() {
triggerOpenedTimer = null;
displayedPermissionDialog = true;
if (accessDialogOpened) { accessDialogOpened(); }
};
var onStream = function(stream) {
finaliseAccessDialog();
success.call(null, stream);
};
var onError = function(event) {
finaliseAccessDialog();
var error = parseErrorEvent(event);
// The error name 'PERMISSION_DENIED' is from an earlier version of the spec
if (error.name === 'PermissionDeniedError' || error.name === 'PermissionDismissedError') {
accessDenied.call(null, error);
} else {
failure.call(null, error);
}
};
if (customGetUserMedia) {
getUserMedia(constraints, onStream, onError);
} else {
getUserMedia(constraints).then(onStream)['catch'](onError);
}
// The 'remember me' functionality of WebRTC only functions over HTTPS, if
// we aren't on HTTPS then we should definitely be displaying the access
// dialog.
//
// If we are on HTTPS, we'll wait 500ms to see if we get a stream
// immediately. If we do then the user had clicked 'remember me'. Otherwise
// we assume that the accessAllowed dialog is visible.
//
// @todo benchmark and see if 500ms is a reasonable number. It seems like
// we should know a lot quicker.
//
if (location.protocol.indexOf('https') === -1) {
// Execute after, this gives the client a chance to bind to the
// accessDialogOpened event.
triggerOpenedTimer = setTimeout(triggerOpened, 100);
} else {
// wait a second and then trigger accessDialogOpened
triggerOpenedTimer = setTimeout(triggerOpened, 500);
}
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"../ot/logging.js":187,"./throttle_until_complete.js":156,"@opentok/ot-helpers":4,"@opentok/otplugin.js":40,"bluebird":70}],151:[function(require,module,exports){
'use strict';
var uuid = require('uuid');
var OTHelpers = require('@opentok/ot-helpers');
var logging = require('../ot/logging.js');
var guidStorage = {};
module.exports = guidStorage;
var currentGuidStorage,
currentGuid;
var isInvalidStorage = function isInvalidStorage(storageInterface) {
return !(
OTHelpers.isFunction(storageInterface.get) &&
OTHelpers.isFunction(storageInterface.set)
);
};
var getClientGuid = function getClientGuid(completion) {
if (currentGuid) {
completion(null, currentGuid);
return;
}
// It's the first time that getClientGuid has been called
// in this page lifetime. Attempt to load any existing Guid
// from the storage
currentGuidStorage.get(completion);
};
/*
* Sets the methods for storing and retrieving client GUIDs persistently
* across sessions. By default, OpenTok.js attempts to use browser cookies to
* store GUIDs.
*
* Pass in an object that has a get() method and
* a set() method.
*
* The get() method must take one parameter: the callback
* method to invoke. The callback method is passed two parameters —
* the first parameter is an error object or null if the call is successful;
* and the second parameter is the GUID (a string) if successful.
*
* The set() method must include two parameters: the GUID to set
* (a string) and the callback method to invoke. The callback method is
* passed an error object on error, or it is passed no parameter if the call is
* successful.
*
* Here is an example: *
*
* var ComplexStorage = function() {
* this.set = function(guid, completion) {
* AwesomeBackendService.set(guid, function(response) {
* completion(response.error || null);
* });
* };
* this.get = function(completion) {
* AwesomeBackendService.get(function(response, guid) {
* completion(response.error || null, guid);
* });
* };
* };
*
* OT.overrideGuidStorage(new ComplexStorage());
*
*/
guidStorage.override = function(storageInterface) {
if (isInvalidStorage(storageInterface)) {
throw new Error('The storageInterface argument does not seem to be valid, ' +
'it must implement get and set methods');
}
if (currentGuidStorage === storageInterface) {
return;
}
currentGuidStorage = storageInterface;
// If a client Guid has already been assigned to this client then
// let the new storage know about it so that it's in sync.
if (currentGuid) {
currentGuidStorage.set(currentGuid, function(error) {
if (error) {
logging.error('Failed to send initial Guid value (' + currentGuid +
') to the newly assigned Guid Storage. The error was: ' + error);
// @todo error
}
});
}
};
guidStorage.get = function(completion) {
getClientGuid(function(error, guid) {
if (error) {
completion(error);
return;
}
if (!guid) {
// Nothing came back, this client is entirely new.
// generate a new Guid and persist it
guid = uuid();
currentGuidStorage.set(guid, function(error) {
if (error) {
completion(error);
return;
}
currentGuid = guid;
});
} else if (!currentGuid) {
currentGuid = guid;
}
completion(null, currentGuid);
});
};
// Implement our default storage mechanism, which sets/gets a cookie
// called 'opentok_client_id'
guidStorage.override({
get: function(completion) {
completion(null, OTHelpers.getCookie('opentok_client_id'));
},
set: function(guid, completion) {
OTHelpers.setCookie('opentok_client_id', guid);
completion(null);
}
});
// Test only
guidStorage.set = function(guid) {
currentGuid = guid;
};
},{"../ot/logging.js":187,"@opentok/ot-helpers":4,"uuid":137}],152:[function(require,module,exports){
(function (global){
'use strict';
// Returns true if we think the DOM has been unloaded
// It detects this by looking for a global object, which
// should always exist until the DOM is cleaned up.
var uuid = require('uuid');
var canary = 'OT_CANARY_' + uuid();
global[canary] = {};
module.exports = function() {
return !global[canary];
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"uuid":137}],153:[function(require,module,exports){
(function (global){
'use strict';
module.exports = function noConflict() {
var globalOT = global.OT;
var globalTB = global.TB;
return function() {
var OT = global.OT;
global.OT = globalOT;
global.TB = globalTB;
return OT;
};
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],154:[function(require,module,exports){
(function (global){
'use strict';
var assign = require('lodash.assign');
var pick = require('lodash.pick');
var OTHelpers = require('@opentok/ot-helpers');
var OTPlugin = require('@opentok/otplugin.js');
var incompleteProperties = require('../../conf/current_properties.js');
// Augments properties from conf. Makes it better and stronger!
var properties = OTHelpers.clone(incompleteProperties);
properties.debug = properties.debug === 'true' || properties.debug === true;
properties.supportSSL = properties.supportSSL === 'true' || properties.supportSSL === true;
if (global.OTProperties) {
// Allow global.OTProperties to override cdnURL, configURL, assetURL and cssURL
// TODO: Can we find a better solution here? This is the only place where I'm allowing this kind
// of global interaction post-modularization.
// override "authorized" properties with the global settings
assign(properties, pick(global.OTProperties, 'cdnURL', 'cdnURLSSL', 'configURL', 'assetURL', 'cssURL'));
}
if (!properties.cdnURLSSL && global.location) {
properties.cdnURLSSL = global.location.protocol + '//' + global.location.host;
}
if (!properties.cdnURL && global.location) {
properties.cdnURL = global.location.protocol + '//' + global.location.host;
}
if (!properties.assetURL) {
if (properties.supportSSL) {
properties.assetURL = properties.cdnURLSSL + '/webrtc/' + properties.version;
} else {
properties.assetURL = properties.cdnURL + '/webrtc/' + properties.version;
}
}
var isIE89 = OTHelpers.env.name === 'IE' && OTHelpers.env.version <= 9;
if (!(isIE89 && global.location.protocol.indexOf('https') < 0)) {
properties.apiURL = properties.apiURLSSL;
properties.loggingURL = properties.loggingURLSSL;
}
OTPlugin.setPathToInstaller(properties.assetURL + '/plugin/');
// at this stage if cssURL is still not defined we consider it an oversight and we are forcing a
// default value
if (!properties.hasOwnProperty('cssURL')) {
properties.cssURL = properties.assetURL + '/css/TB.min.css';
}
module.exports = properties;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"../../conf/current_properties.js":139,"@opentok/ot-helpers":4,"@opentok/otplugin.js":40,"lodash.assign":78,"lodash.pick":121}],155:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
// Returns a String representing the supported WebRTC crypto scheme. The possible
// values are SDES_SRTP, DTLS_SRTP, and NONE;
//
// Broadly:
// * Firefox only supports DTLS
// * Older versions of Chrome (<= 24) only support SDES
// * Newer versions of Chrome (>= 25) support DTLS and SDES
//
module.exports = function supportedCryptoScheme() {
var chromeBefore25 = (
OTHelpers.env.name === 'Chrome' &&
OTHelpers.env.version < 25
);
return (chromeBefore25 ? 'SDES_SRTP' : 'DTLS_SRTP');
};
},{"@opentok/ot-helpers":4}],156:[function(require,module,exports){
'use strict';
var Bluebird = require('bluebird');
// Ensures that promisified functions run in serial.
// Perhaps this module should be called "serialize," but that seems too generic.
//
// @memberof OTHelpers
// @private
//
// @return The throttled function.
//
module.exports = function(fn) {
// We start our queue with a resolved promise. We do this
// because we want queue to be thenable, but we also want
// it to execute the first element of the queue immediately.
var queue = Bluebird.Promise.resolve();
return function() {
var args = Array.prototype.slice.call(arguments);
queue = queue.then(function() {
// execute our function with args when the previous
// steps have finished.
return fn.apply(null, args);
});
return queue;
};
};
},{"bluebird":70}],157:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
module.exports = function(self, domElement) {
var width = domElement.videoWidth;
var height = domElement.videoHeight;
var stopped = true;
function actor() {
if (stopped) {
return;
}
if (width !== domElement.videoWidth || height !== domElement.videoHeight) {
self.trigger('videoDimensionsChanged',
{ width: width, height: height },
{ width: domElement.videoWidth, height: domElement.videoHeight }
);
width = domElement.videoWidth;
height = domElement.videoHeight;
}
waiter();
}
function waiter() {
self.whenTimeIncrements(function() {
OTHelpers.requestAnimationFrame(actor);
});
}
self.startObservingSize = function() {
stopped = false;
waiter();
};
self.stopObservingSize = function() {
stopped = true;
};
};
},{"@opentok/ot-helpers":4}],158:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
var VideoOrientationTransforms = {
0: 'rotate(0deg)',
270: 'rotate(90deg)',
90: 'rotate(-90deg)',
180: 'rotate(180deg)'
};
// A mixin to create the orientation API implementation on +self+
// +getDomElementCallback+ is a function that the mixin will call when it wants to
// get the native Dom element for +self+.
//
// +initialOrientation+ sets the initial orientation (shockingly), it's currently unused
// so the initial value is actually undefined.
//
module.exports = function canBeOrientatedMixin(
self,
getDomElementCallback,
orientationChangedHandler,
initialOrientation
) {
var _orientation = initialOrientation;
OTHelpers.defineProperties(self, {
isRotated: {
get: function() {
return this.orientation() &&
(this.orientation().videoOrientation === 270 ||
this.orientation().videoOrientation === 90);
}
},
orientation: {
get: function() { return _orientation; },
set: function(orientation) {
_orientation = orientation;
var transform = VideoOrientationTransforms[orientation.videoOrientation] ||
VideoOrientationTransforms.ROTATED_NORMAL;
switch (OTHelpers.env.name) {
case 'Chrome':
case 'Safari':
getDomElementCallback().style.webkitTransform = transform;
break;
case 'IE':
getDomElementCallback().style.msTransform = transform;
break;
default:
// The standard version, just Firefox, Opera, and IE > 9
getDomElementCallback().style.transform = transform;
}
orientationChangedHandler(_orientation);
}
},
// see https://wiki.mozilla.org/WebAPI/AudioChannels
// The audioChannelType is currently only available in Firefox. This property returns
// "unknown" in other browser. The related HTML tag attribute is "mozaudiochannel"
audioChannelType: {
get: function() {
if ('mozAudioChannelType' in this.domElement) {
return this.domElement.mozAudioChannelType;
}
return 'unknown';
},
set: function(type) {
if ('mozAudioChannelType' in this.domElement) {
this.domElement.mozAudioChannelType = type;
}
}
}
});
};
},{"@opentok/ot-helpers":4}],159:[function(require,module,exports){
'use strict';
var OTPlugin = require('@opentok/otplugin.js');
var OTHelpers = require('@opentok/ot-helpers');
var NativeVideoElementWrapper = require('./native_video_element_wrapper');
var PluginVideoElementWrapper = require('./plugin_video_element_wrapper');
var defaultAudioVolume = 50;
//
//
// var _videoElement = new VideoElement({
// fallbackText: 'blah'
// }, errorHandler);
//
// _videoElement.bindToStream(webRtcStream, completion); // => VideoElement
// _videoElement.appendTo(DOMElement) // => VideoElement
//
// _videoElement.domElement // => DomNode
//
// _videoElement.imgData // => PNG Data string
//
// _videoElement.orientation = VideoOrientation.ROTATED_LEFT;
//
// _videoElement.unbindStream();
// _videoElement.destroy() // => Completely cleans up and
// removes the video element
//
//
module.exports = function VideoElementFacade(
/* optional */ options/*,
optional errorHandler*/
) {
var _stream, _preInitialisedVolume;
var _streamBound = false;
var _options = OTHelpers.defaults(options && !OTHelpers.isFunction(options) ? options : {}, {
fallbackText: 'Sorry, Web RTC is not available in your browser'
});
var errorHandler = OTHelpers.isFunction(arguments[arguments.length - 1]) ?
arguments[arguments.length - 1] : void 0;
var orientationHandler = function(orientation) {
this.trigger('orientationChanged', orientation);
}.bind(this);
var _videoElementWrapper = (OTPlugin.isInstalled() ?
new PluginVideoElementWrapper(_options, errorHandler, orientationHandler, defaultAudioVolume) :
new NativeVideoElementWrapper(_options, errorHandler, orientationHandler, defaultAudioVolume)
);
OTHelpers.eventing(this);
_videoElementWrapper.on('videoDimensionsChanged', function(oldValue, newValue) {
this.trigger('videoDimensionsChanged', oldValue, newValue);
}.bind(this));
_videoElementWrapper.on('mediaStopped', function() {
this.trigger('mediaStopped');
}.bind(this));
_videoElementWrapper.on('videoElementCreated', function(element) {
this.trigger('videoElementCreated', element);
}.bind(this));
// Public Properties
OTHelpers.defineProperties(this, {
domElement: {
get: function() {
return _videoElementWrapper.domElement();
}
},
videoWidth: {
get: function() {
return _videoElementWrapper['video' + (this.isRotated() ? 'Height' : 'Width')]();
}
},
videoHeight: {
get: function() {
return _videoElementWrapper['video' + (this.isRotated() ? 'Width' : 'Height')]();
}
},
aspectRatio: {
get: function() {
return (this.videoWidth() + 0.0) / this.videoHeight();
}
},
isRotated: {
get: function() {
return _videoElementWrapper.isRotated();
}
},
orientation: {
get: function() {
return _videoElementWrapper.orientation();
},
set: function(orientation) {
_videoElementWrapper.orientation(orientation);
}
},
audioChannelType: {
get: function() {
return _videoElementWrapper.audioChannelType();
},
set: function(type) {
_videoElementWrapper.audioChannelType(type);
}
}
});
// Public Methods
this.imgData = function() {
return _videoElementWrapper.imgData();
};
this.appendTo = function(parentDomElement) {
_videoElementWrapper.appendTo(parentDomElement);
return this;
};
this.bindToStream = function(webRtcStream, completion) {
_streamBound = false;
_stream = webRtcStream;
_videoElementWrapper.bindToStream(webRtcStream, function(err) {
if (err) {
completion(err);
return;
}
_streamBound = true;
if (typeof _preInitialisedVolume !== 'undefined') {
this.setAudioVolume(_preInitialisedVolume);
_preInitialisedVolume = undefined;
}
_videoElementWrapper.on('aspectRatioAvailable',
this.trigger.bind(this, 'aspectRatioAvailable'));
completion(null);
}.bind(this));
return this;
};
this.unbindStream = function() {
if (!_stream) { return this; }
_stream.onended = null;
_stream = null;
_videoElementWrapper.unbindStream();
return this;
};
this.setAudioVolume = function(value) {
if (_streamBound) {
_videoElementWrapper.setAudioVolume(OTHelpers.roundFloat(value / 100, 2));
} else {
_preInitialisedVolume = parseFloat(value);
}
return this;
};
this.getAudioVolume = function() {
if (_streamBound) {
return parseInt(_videoElementWrapper.getAudioVolume() * 100, 10);
}
if (typeof _preInitialisedVolume !== 'undefined') {
return _preInitialisedVolume;
}
return 50;
};
this.getAudioInputLevel = function() {
return _videoElementWrapper.getAudioInputLevel();
};
this.whenTimeIncrements = function(callback, context) {
_videoElementWrapper.whenTimeIncrements(callback, context);
return this;
};
this.destroy = function() {
// unbind all events so they don't fire after the object is dead
this.off();
_videoElementWrapper.destroy();
return void 0;
};
};
},{"./native_video_element_wrapper":160,"./plugin_video_element_wrapper":161,"@opentok/ot-helpers":4,"@opentok/otplugin.js":40}],160:[function(require,module,exports){
(function (global){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
var logging = require('../../ot/logging.js');
var Promise = require('bluebird');
var canBeOrientatedMixin = require('./can_be_oriented_mixin.js');
var videoElementErrorCodeToStr = require('./video_element_error_code_to_str.js');
var videoContentResizesMixin = require('../video_content_resizes_mixin.js');
var audioContextProvider = require('../audio_context');
var WebaudioAudioLevelSampler = require('../audio_level_samplers/webaudio_audio_level_sampler');
function createNativeVideoElement(fallbackText, muted) {
var videoElement = document.createElement('video');
videoElement.setAttribute('autoplay', '');
videoElement.innerHTML = fallbackText;
if (muted === true) {
videoElement.muted = 'true';
}
return videoElement;
}
function unbindNativeStream(videoElement) {
videoElement.onended = null;
if (videoElement.srcObject !== void 0) {
videoElement.srcObject = null;
} else if (videoElement.mozSrcObject !== void 0) {
videoElement.mozSrcObject = null;
} else {
global.URL.revokeObjectURL(videoElement.src);
}
}
function bindNativeStream(videoElement, webRtcStream) {
// The official spec way is 'srcObject', we are slowly converging there.
if (videoElement.srcObject !== void 0) {
videoElement.srcObject = webRtcStream;
} else if (videoElement.mozSrcObject !== void 0) {
videoElement.mozSrcObject = webRtcStream;
} else {
videoElement.src = global.URL.createObjectURL(webRtcStream);
}
}
function bindStreamToNativeVideoElement(videoElement, webRtcStream, completion) {
var cleanup = function cleanup() {
videoElement.removeEventListener('loadedmetadata', onLoad, false);
videoElement.removeEventListener('error', onError, false);
webRtcStream.onended = null;
videoElement.onended = null;
};
var onLoad = function onLoad() {
cleanup();
completion(null);
};
var onError = function onError(event) {
cleanup();
unbindNativeStream(videoElement);
completion('There was an unexpected problem with the Video Stream: ' +
videoElementErrorCodeToStr(event.target.error.code));
};
var onStoppedLoading = function onStoppedLoading() {
// The stream ended before we fully bound it. Maybe the other end called
// stop on it or something else went wrong.
cleanup();
unbindNativeStream(videoElement);
completion('Stream ended while trying to bind it to a video element.');
};
bindNativeStream(videoElement, webRtcStream);
videoElement.addEventListener('loadedmetadata', onLoad, false);
videoElement.addEventListener('error', onError, false);
webRtcStream.onended = onStoppedLoading;
videoElement.onended = onStoppedLoading;
}
module.exports = function NativeVideoElementWrapper(
options,
errorHandler,
orientationChangedHandler,
defaultAudioVolume
) {
var _domElement, _audioLevelSampler;
var _videoElementMovedWarning = false;
var _destroyed = false;
OTHelpers.eventing(this);
/// Private API
var _onVideoError = function(event) {
var reason = 'There was an unexpected problem with the Video Stream: ' +
videoElementErrorCodeToStr(event.target.error.code);
errorHandler(reason, this, 'VideoElement');
}.bind(this);
// The video element pauses itself when it's reparented, this is
// unfortunate. This function plays the video again and is triggered
// on the pause event.
var _playVideoOnPause = function() {
if (!_videoElementMovedWarning) {
logging.warn('Video element paused, auto-resuming. If you intended to do this, ' +
'use publishVideo(false) or subscribeToVideo(false) instead.');
_videoElementMovedWarning = true;
}
_domElement.play();
};
_domElement = createNativeVideoElement(options.fallbackText, options.muted);
this.trigger('videoElementCreated', _domElement);
// dirty but it is the only way right now to get the aspect ratio in FF
// any other event is triggered too early
var ratioAvailableListeners = [];
_domElement.addEventListener('timeupdate', function timeupdateHandler(event) {
if (!_domElement) {
event.target.removeEventListener('timeupdate', timeupdateHandler);
return;
}
var aspectRatio = _domElement.videoWidth / _domElement.videoHeight;
if (!isNaN(aspectRatio)) {
_domElement.removeEventListener('timeupdate', timeupdateHandler);
var listener;
while ((listener = ratioAvailableListeners.shift())) {
listener();
}
}
});
_domElement.addEventListener('pause', _playVideoOnPause);
videoContentResizesMixin(this, _domElement);
canBeOrientatedMixin(this, function() { return _domElement; }, orientationChangedHandler);
/// Public methods
this.domElement = function() {
return _domElement;
};
this.videoWidth = function() {
return _domElement ? _domElement.videoWidth : 0;
};
this.videoHeight = function() {
return _domElement ? _domElement.videoHeight : 0;
};
this.imgData = function() {
var canvas = OTHelpers.createElement('canvas', {
width: _domElement.videoWidth,
height: _domElement.videoHeight,
style: { display: 'none' }
});
document.body.appendChild(canvas);
try {
canvas.getContext('2d').drawImage(_domElement, 0, 0, canvas.width, canvas.height);
} catch (err) {
logging.warn('Cannot get image data yet');
return null;
}
var imgData = canvas.toDataURL('image/png');
OTHelpers.removeElement(canvas);
return imgData.replace('data:image/png;base64,', '').trim();
};
// Append the Video DOM element to a parent node
this.appendTo = function(parentDomElement) {
parentDomElement.appendChild(_domElement);
return this;
};
// Bind a stream to the video element.
this.bindToStream = function(webRtcStream, completion) {
var _this = this;
bindStreamToNativeVideoElement(_domElement, webRtcStream, function(err) {
if (_destroyed) {
// the operation has been canceled
return;
}
if (err) {
completion(err);
return;
}
if (!_domElement) {
completion('Can\'t bind because _domElement no longer exists');
return;
}
_this.startObservingSize();
function handleEnded() {
webRtcStream.onended = null;
if (_domElement) {
_domElement.onended = null;
}
_this.trigger('mediaStopped', _this);
}
// OPENTOK-22428: Firefox doesn't emit the ended event on the webRtcStream when the user
// stops sharing their camera, but we do get the ended event on the video element.
webRtcStream.onended = handleEnded;
_domElement.onended = handleEnded;
_domElement.addEventListener('error', _onVideoError, false);
if (webRtcStream.getAudioTracks().length > 0) {
_audioLevelSampler = new WebaudioAudioLevelSampler(audioContextProvider());
_audioLevelSampler.webRTCStream(webRtcStream);
}
completion(null);
});
return this;
};
// Unbind the currently bound stream from the video element.
this.unbindStream = function() {
if (_domElement) {
unbindNativeStream(_domElement);
}
this.stopObservingSize();
_audioLevelSampler = null;
return this;
};
this.setAudioVolume = function(value) {
if (_domElement) { _domElement.volume = value; }
};
this.getAudioVolume = function() {
// Return the actual volume of the DOM element
if (_domElement) { return _domElement.volume; }
return defaultAudioVolume;
};
// see https://wiki.mozilla.org/WebAPI/AudioChannels
// The audioChannelType is currently only available in Firefox. This property returns
// "unknown" in other browser. The related HTML tag attribute is "mozaudiochannel"
this.audioChannelType = function(type) {
if (type !== void 0) {
_domElement.mozAudioChannelType = type;
}
if ('mozAudioChannelType' in _domElement) {
return _domElement.mozAudioChannelType;
}
return 'unknown';
};
this.getAudioInputLevel = function() {
return new Promise(function(resolve) {
if (_audioLevelSampler) {
_audioLevelSampler.sample(resolve);
} else {
// if we can't sample a value we still resolve but with a null value
// it is up to the caller to figure out what to do with it
resolve(null);
}
});
};
this.whenTimeIncrements = function(callback, context) {
if (_domElement) {
var lastTime, handler;
handler = function() {
if (_domElement) {
if (!lastTime || lastTime >= _domElement.currentTime) {
lastTime = _domElement.currentTime;
} else {
_domElement.removeEventListener('timeupdate', handler, false);
callback.call(context, this);
}
}
}.bind(this);
_domElement.addEventListener('timeupdate', handler, false);
}
};
this.destroy = function() {
_destroyed = true;
this.unbindStream();
if (_domElement) {
// Unbind this first, otherwise it will trigger when the
// video element is removed from the DOM.
_domElement.removeEventListener('pause', _playVideoOnPause);
OTHelpers.removeElement(_domElement);
_domElement = null;
}
return void 0;
};
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"../../ot/logging.js":187,"../audio_context":141,"../audio_level_samplers/webaudio_audio_level_sampler":143,"../video_content_resizes_mixin.js":157,"./can_be_oriented_mixin.js":158,"./video_element_error_code_to_str.js":162,"@opentok/ot-helpers":4,"bluebird":70}],161:[function(require,module,exports){
'use strict';
var Promise = require('bluebird');
var OTHelpers = require('@opentok/ot-helpers');
var canBeOrientatedMixin = require('./can_be_oriented_mixin.js');
module.exports = function PluginVideoElementWrapper(
options,
errorHandler,
orientationChangedHandler,
defaultAudioVolume
) {
var _pluginVideoContainer, _parentDomElement;
OTHelpers.eventing(this);
canBeOrientatedMixin(this, function() {
return _pluginVideoContainer.domElement;
},
orientationChangedHandler);
/// Public methods
this.domElement = function() {
return _pluginVideoContainer ? _pluginVideoContainer.domElement : void 0;
};
this.videoWidth = function() {
return _pluginVideoContainer ? _pluginVideoContainer.videoWidth() : void 0;
};
this.videoHeight = function() {
return _pluginVideoContainer ? _pluginVideoContainer.videoHeight() : void 0;
};
this.imgData = function() {
return _pluginVideoContainer ? _pluginVideoContainer.getImgData() : null;
};
// Append the Video DOM element to a parent node
this.appendTo = function(parentDomElement) {
_parentDomElement = parentDomElement;
return this;
};
// Bind a stream to the video element.
this.bindToStream = function(webRtcStream, completion) {
if (!_parentDomElement) {
completion('The VideoElement must attached to a DOM node before a stream can be bound');
return undefined;
}
_pluginVideoContainer = webRtcStream._.render();
this.trigger('videoElementCreated', this.domElement());
_pluginVideoContainer.setFitMode(options.fitMode);
_pluginVideoContainer.appendTo(_parentDomElement);
_pluginVideoContainer.show(function(error) {
completion(error);
});
return this;
};
// Unbind the currently bound stream from the video element.
this.unbindStream = function() {
// TODO: some way to tell OTPlugin to release that stream and controller
if (_pluginVideoContainer) {
_pluginVideoContainer.destroy();
if (this.domElement() && this.domElement().parentNode) {
this.domElement().parentNode.removeChild(this.domElement());
}
_parentDomElement = null;
_pluginVideoContainer = null;
}
return this;
};
this.setAudioVolume = function(value) {
if (_pluginVideoContainer) { _pluginVideoContainer.volume(value); }
};
this.getAudioVolume = function() {
// Return the actual volume of the DOM element
if (_pluginVideoContainer) { return _pluginVideoContainer.volume(); }
return defaultAudioVolume;
};
// see https://wiki.mozilla.org/WebAPI/AudioChannels
// The audioChannelType is not currently supported in the plugin.
this.audioChannelType = function(/* type */) {
return 'unknown';
};
this.getAudioInputLevel = function() {
return Promise.resolve(_pluginVideoContainer.getAudioInputLevel());
};
this.whenTimeIncrements = function(callback, context) {
// exists for compatibility with NativeVideoElement
OTHelpers.callAsync(callback.bind(context));
};
this.destroy = function() {
this.unbindStream();
return void 0;
};
};
},{"./can_be_oriented_mixin.js":158,"@opentok/ot-helpers":4,"bluebird":70}],162:[function(require,module,exports){
(function (global){
'use strict';
// See http://www.w3.org/TR/2010/WD-html5-20101019/video.html#error-codes
var _videoErrorCodes = {};
// Checking for global.MediaError for IE compatibility, just so we don't throw
// exceptions when the script is included
if (global.MediaError) {
_videoErrorCodes[global.MediaError.MEDIA_ERR_ABORTED] = 'The fetching process for the media ' +
'resource was aborted by the user agent at the user\'s request.';
_videoErrorCodes[global.MediaError.MEDIA_ERR_NETWORK] = 'A network error of some description ' +
'caused the user agent to stop fetching the media resource, after the resource was ' +
'established to be usable.';
_videoErrorCodes[global.MediaError.MEDIA_ERR_DECODE] = 'An error of some description ' +
'occurred while decoding the media resource, after the resource was established to be ' +
' usable.';
_videoErrorCodes[global.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED] = 'The media resource ' +
'indicated by the src attribute was not suitable.';
}
module.exports = function videoElementErrorCodeToStr(errorCode) {
return _videoErrorCodes[parseInt(errorCode, 10)] || 'An unknown error occurred.';
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],163:[function(require,module,exports){
'use strict';
module.exports = {
ROTATED_NORMAL: 0,
ROTATED_LEFT: 270,
ROTATED_RIGHT: 90,
ROTATED_UPSIDE_DOWN: 180
};
},{}],164:[function(require,module,exports){
'use strict';
var uuid = require('uuid');
var OTPlugin = require('@opentok/otplugin.js');
var OTHelpers = require('@opentok/ot-helpers');
var logging = require('../ot/logging.js');
var VideoElementFacade = require('./video_element/index.js');
var miniWidth = 128;
var miniHeight = 128;
var microWidth = 64;
var microHeight = 64;
function fixMini(container, width, height) {
var w = parseInt(width, 10);
var h = parseInt(height, 10);
if (w < microWidth || h < microHeight) {
OTHelpers.addClass(container, 'OT_micro');
} else {
OTHelpers.removeClass(container, 'OT_micro');
}
if (w < miniWidth || h < miniHeight) {
OTHelpers.addClass(container, 'OT_mini');
} else {
OTHelpers.removeClass(container, 'OT_mini');
}
}
var getOrCreateContainer = function getOrCreateContainer(elementOrDomId, insertMode) {
var container,
domId;
if (elementOrDomId && elementOrDomId.nodeName) {
// It looks like we were given a DOM element. Grab the id or generate
// one if it doesn't have one.
container = elementOrDomId;
if (!container.getAttribute('id') || container.getAttribute('id').length === 0) {
container.setAttribute('id', 'OT_' + uuid());
}
domId = container.getAttribute('id');
} else if (elementOrDomId) {
// We may have got an id, try and get it's DOM element.
container = OTHelpers('#' + elementOrDomId).get(0);
if (container) { domId = elementOrDomId; }
}
if (!domId) {
domId = 'OT_' + uuid().replace(/-/g, '_');
}
if (!container) {
container = OTHelpers.createElement('div', { id: domId });
container.style.backgroundColor = '#000000';
document.body.appendChild(container);
} else if (!(insertMode == null || insertMode === 'replace')) {
var placeholder = document.createElement('div');
placeholder.id = ('OT_' + uuid());
if (insertMode === 'append') {
container.appendChild(placeholder);
container = placeholder;
} else if (insertMode === 'before') {
container.parentNode.insertBefore(placeholder, container);
container = placeholder;
} else if (insertMode === 'after') {
container.parentNode.insertBefore(placeholder, container.nextSibling);
container = placeholder;
}
} else {
OTHelpers.emptyElement(container);
}
return container;
};
// Creates the standard container that the Subscriber and Publisher use to hold
// their video element and other chrome.
var WidgetView = function(targetElement, properties) {
var widgetView = {};
var oldContainerStyles = {};
var loading = true;
var audioOnly = false;
var fitMode = 'cover';
var sizeObserver,
_videoElementFacade,
videoObserver,
posterContainer,
loadingContainer,
width,
height,
container;
if (properties.insertDefaultUI !== false) {
container = getOrCreateContainer(targetElement, properties && properties.insertMode);
}
var widgetContainer = document.createElement('div');
OTHelpers.eventing(widgetView);
if (properties && container) {
width = properties.width;
height = properties.height;
if (width) {
if (typeof width === 'number') {
width += 'px';
}
}
if (height) {
if (typeof height === 'number') {
height += 'px';
}
}
container.style.width = width ? width : '264px';
container.style.height = height ? height : '198px';
container.style.overflow = 'hidden';
fixMini(container, width || '264px', height || '198px');
if (properties.mirror) {
OTHelpers.addClass(container, 'OT_mirrored');
}
if (properties.fitMode === 'contain') {
fitMode = 'contain';
} else if (properties.fitMode !== 'cover') {
logging.warn('Invalid fit value "' + properties.fitMode + '" passed. ' +
'Only "contain" and "cover" can be used.');
}
if (properties.classNames) { OTHelpers.addClass(container, properties.classNames); }
OTHelpers(container).addClass('OT_loading OT_fit-mode-' + fitMode);
}
OTHelpers.addClass(widgetContainer, 'OT_widget-container');
widgetContainer.style.width = '100%'; //container.style.width;
widgetContainer.style.height = '100%'; // container.style.height;
if (container) {
container.appendChild(widgetContainer);
}
loadingContainer = document.createElement('div');
OTHelpers.addClass(loadingContainer, 'OT_video-loading');
widgetContainer.appendChild(loadingContainer);
posterContainer = document.createElement('div');
OTHelpers.addClass(posterContainer, 'OT_video-poster');
widgetContainer.appendChild(posterContainer);
if (container) {
oldContainerStyles.width = container.offsetWidth;
oldContainerStyles.height = container.offsetHeight;
}
if (!OTPlugin.isInstalled() && container) {
// Observe changes to the width and height and update the aspect ratio
sizeObserver = OTHelpers(container).observeSize(
function(size) {
var width = size.width;
var height = size.height;
fixMini(container, width, height);
})[0];
// @todo observe if the video container or the video element get removed
// if they do we should do some cleanup
videoObserver = OTHelpers.observeNodeOrChildNodeRemoval(container, function(removedNodes) {
if (!_videoElementFacade) { return; }
// This assumes a video element being removed is the main video element. This may
// not be the case.
var videoRemoved = removedNodes.some(function(node) {
return node === widgetContainer || node.nodeName === 'VIDEO';
});
if (videoRemoved) {
_videoElementFacade.destroy();
_videoElementFacade = null;
}
if (widgetContainer) {
OTHelpers.removeElement(widgetContainer);
widgetContainer = null;
}
if (sizeObserver) {
sizeObserver.disconnect();
sizeObserver = null;
}
if (videoObserver) {
videoObserver.disconnect();
videoObserver = null;
}
});
}
widgetView.destroy = function() {
if (sizeObserver) {
sizeObserver.disconnect();
sizeObserver = null;
}
if (videoObserver) {
videoObserver.disconnect();
videoObserver = null;
}
if (_videoElementFacade) {
_videoElementFacade.destroy();
_videoElementFacade = null;
}
if (container) {
OTHelpers.removeElement(container);
container = null;
}
};
widgetView.setBackgroundImageURI = function(bgImgURI) {
OTHelpers.css(posterContainer, 'backgroundImage', 'url(' + bgImgURI + ')');
OTHelpers.css(posterContainer, 'backgroundSize', 'contain');
OTHelpers.css(posterContainer, 'opacity', '1.0');
};
if (properties && properties.style && properties.style.backgroundImageURI) {
widgetView.setBackgroundImageURI(properties.style.backgroundImageURI);
}
/**
* @returns {VideoElementFacade}
*/
widgetView.bindVideo = function(webRTCStream, options, completion) {
// remove the old video element if it exists
// @todo this might not be safe, publishers/subscribers use this as well...
options.fitMode = fitMode;
if (typeof options.audioVolume !== 'undefined') {
options.audioVolume = parseFloat(options.audioVolume);
}
if (_videoElementFacade) {
_videoElementFacade.destroy();
_videoElementFacade = null;
}
var onError = options && options.error ? options.error : void 0;
delete options.error;
var newVideoElementFacade = new WidgetView.VideoElementFacade(options, onError);
if (newVideoElementFacade.domElement()) {
widgetView.trigger('videoElementCreated', newVideoElementFacade.domElement());
}
newVideoElementFacade.on('videoElementCreated', function(element) {
widgetView.trigger('videoElementCreated', element);
});
// Initialize the audio volume
if (typeof options.audioVolume !== 'undefined') {
newVideoElementFacade.setAudioVolume(options.audioVolume);
}
// makes the incoming audio streams take priority (will impact only FF OS for now)
newVideoElementFacade.audioChannelType('telephony');
newVideoElementFacade.appendTo(widgetContainer);
newVideoElementFacade.bindToStream(webRTCStream, function(err) {
if (err) {
newVideoElementFacade.destroy();
completion(err);
return;
}
_videoElementFacade = newVideoElementFacade;
newVideoElementFacade.on({
videoDimensionsChanged: function(oldValue, newValue) {
widgetView.trigger('videoDimensionsChanged', oldValue, newValue);
},
mediaStopped: function() {
widgetView.trigger('mediaStopped');
}
});
completion(null, newVideoElementFacade);
});
if (properties.insertDefaultUI !== false) {
OTHelpers.addClass(newVideoElementFacade.domElement(), 'OT_video-element');
}
return newVideoElementFacade;
};
OTHelpers.defineProperties(widgetView, {
video: {
get: function() {
return _videoElementFacade;
}
},
showPoster: {
get: function() {
return !OTHelpers.isDisplayNone(posterContainer);
},
set: function(newValue) {
if (newValue) {
OTHelpers.show(posterContainer);
} else {
OTHelpers.hide(posterContainer);
}
}
},
poster: {
get: function() {
return OTHelpers.css(posterContainer, 'backgroundImage');
},
set: function(src) {
OTHelpers.css(posterContainer, 'backgroundImage', 'url(' + src + ')');
}
},
loading: {
get: function() { return loading; },
set: function(l) {
loading = l;
if (container) {
if (loading) {
OTHelpers.addClass(container, 'OT_loading');
} else {
OTHelpers.removeClass(container, 'OT_loading');
}
}
}
},
audioOnly: {
get: function() { return audioOnly; },
set: function(a) {
audioOnly = a;
if (audioOnly) {
OTHelpers.addClass(container, 'OT_audio-only');
} else {
OTHelpers.removeClass(container, 'OT_audio-only');
}
}
},
domId: {
get: function() { return container && container.getAttribute('id'); }
}
});
widgetView.domElement = container;
widgetView.addError = function(errorMsg, helpMsg, classNames) {
if (container) {
container.innerHTML = '' + errorMsg + (helpMsg ? ' ' : '') + '
'; OTHelpers.addClass(container, classNames || 'OT_subscriber_error'); if (container.querySelector('p').offsetHeight > container.offsetHeight) { container.querySelector('span').style.display = 'none'; } } }; return widgetView; }; // This is bound here so that it can be mocked in testing. Feels like a smell that's a symptom of // larger problems to me, but I'm just maintaining existing behaviour right now. WidgetView.VideoElementFacade = VideoElementFacade; module.exports = WidgetView; },{"../ot/logging.js":187,"./video_element/index.js":159,"@opentok/ot-helpers":4,"@opentok/otplugin.js":40,"uuid":137}],165:[function(require,module,exports){ (function (global){ 'use strict'; // We need to do this first because of circular dependency issues with otplugin.js. @TODO: fix this. var OT = {}; global.OT = OT; global.TB = OT; module.exports = OT; var OTHelpers = require('@opentok/ot-helpers'); // This is not obvious, so to prevent end-user frustration we'll let them know // explicitly rather than failing with a bunch of permission errors. We don't // handle this using an OT Exception as it's really only a development thing. var shouldAlertAboutFileProtocol = ( OTHelpers.env.name !== 'Node' && global.location.protocol === 'file:' && typeof window.cordova === 'undefined' ); if (shouldAlertAboutFileProtocol) { global.alert('You cannot test a page using WebRTC through the file system due to browser ' + 'permissions. You must run it over a web server.'); } // TODO: This is added to OTHelpers for consistency with the capabilities stuff. Capabilities' days // are numbered though. OTHelpers.getUserMedia = require('./helpers/get_user_media.js'); var Analytics = require('./helpers/analytics.js'); var APIKEY = require('./ot/api_key.js'); var AudioLevelTransformer = require('./ot/audio_level_transformer'); var WebaudioAudioLevelSampler = require('./helpers/audio_level_samplers/webaudio_audio_level_sampler'); var GetstatsAudioOutputLevelSampler = require('./helpers/audio_level_samplers/getstats_audio_output_level_sampler'); var cssLoader = require('./helpers/css_loader.js'); var Dialogs = require('./helpers/dialogs.js'); var EnvironmentLoader = require('./ot/environment_loader.js'); var Events = require('./ot/events.js'); var guidStorage = require('./helpers/guid_storage.js'); var initSession = require('./ot/session/init.js'); var logging = require('./ot/logging.js'); var OTError = require('./ot/ot_error.js'); var properties = require('./helpers/properties.js'); var screenSharing = require('./ot/screensharing/screen_sharing.js'); var sessionObjects = require('./ot/session/objects.js'); var systemRequirements = require('./ot/system_requirements.js'); // Allow events to be bound on OT OTHelpers.eventing(OT); OT.$ = OTHelpers; // Define the APIKEY this is a global parameter which should not change OT.APIKEY = APIKEY.value; OT.AnalyserAudioLevelSampler = WebaudioAudioLevelSampler; // TODO: Fix dodgy use of this by OTPlugin. It may also need to be exposed here for public use, // hopefully not. OT.Analytics = Analytics; OT.Anvil = require('./ot/anvil.js'); OT.Archive = require('./ot/archive.js'); OT.ArchiveEvent = Events.ArchiveEvent; OT.ArchiveUpdatedEvent = Events.ArchiveUpdatedEvent; OT.AudioLevelTransformer = AudioLevelTransformer; OT.AudioLevelUpdatedEvent = Events.AudioLevelUpdatedEvent; OT.Capabilities = require('./ot/capabilities.js'); OT.Chrome = require('./ot/chrome/chrome.js'); OT.Connection = require('./ot/connection.js'); OT.ConnectionCapabilities = OT.Connection.Capabilities; OT.ConnectionEvent = Events.ConnectionEvent; OT.ConnectivityAttemptPinger = require('./helpers/connectivity_attempt_pinger.js'); OT.DEBUG = logging.DEBUG; OT.DestroyedEvent = Events.DestroyedEvent; // TODO: This is here because of OTPlugin. Another circular dependency issue we need to sort out. OT.Dialogs = Dialogs; OT.ERROR = logging.ERROR; OT.EnvLoadedEvent = Events.EnvLoadedEvent; // TODO: re-expose old screenSharing api OT.Error = OTError; OT.Error.on(Events.Event.names.EXCEPTION, function(exceptionEvent) { if (exceptionEvent.target === OT.Error) { // Rebind target to OT if it's set to OT.Error to preserve old behaviour. var exceptionEventClone = OTHelpers.clone(exceptionEvent); exceptionEventClone.target = OT; OT.dispatchEvent(exceptionEventClone); } else { OT.dispatchEvent(exceptionEvent); } }); OT.Event = Events.Event; OT.ExceptionCodes = require('./ot/exception_codes.js'); OT.ExceptionEvent = Events.ExceptionEvent; OT.getDevices = require('./ot/get_devices.js'); OT.GetStatsAudioLevelSampler = GetstatsAudioOutputLevelSampler; OT.HAS_REQUIREMENTS = 1; OT.INFO = logging.INFO; OT.IntervalRunner = require('./ot/interval_runner.js'); OT.IssueReportedEvent = Events.IssueReportedEvent; OT.LOG = logging.LOG; OT.MediaStoppedEvent = Events.MediaStoppedEvent; OT.Microphone = require('./ot/publisher/microphone.js'); OT.Modal = OTHelpers.Modal; OT.NONE = logging.NONE; OT.NOT_HAS_REQUIREMENTS = 0; OT.PeerConnection = require('./ot/peer_connection/peer_connection.js'); // TODO: this is here for repel, but it doesn't belong here OT.PeerConnection.QOS = require('./ot/peer_connection/qos.js'); OT.PeerConnections = require('./ot/peer_connection/peer_connections.js'); OT.Publisher = require('./ot/publisher'); OT.PublisherPeerConnection = require('./ot/peer_connection/publisher_peer_connection.js'); OT.PublishingState = require('./ot/publisher/state.js'); OT.Raptor = require('./ot/messaging/raptor/legacy_structure.js'); OT.Rumor = require('./ot/messaging/rumor/legacy_structure.js'); OT.Session = require('./ot/session/handle.js').Session; OT.SessionConnectEvent = Events.SessionConnectEvent; OT.SessionDisconnectEvent = Events.SessionDisconnectEvent; OT.SessionDispatcher = require('./ot/messaging/raptor/session_dispatcher.js'); OT.SessionInfo = require('./ot/session/info.js'); OT.Signal = require('./ot/messaging/raptor/signal.js'); OT.SignalEvent = Events.SignalEvent; OT.Stream = require('./ot/stream.js'); OT.StreamChannel = require('./ot/stream_channel.js'); OT.StreamEvent = Events.StreamEvent; OT.StreamPropertyChangedEvent = Events.StreamPropertyChangedEvent; OT.StreamUpdatedEvent = Events.StreamUpdatedEvent; OT.StylableComponent = require('./ot/styling/stylable_component.js'); OT.Subscriber = require('./ot/subscriber'); OT.SubscriberPeerConnection = require('./ot/peer_connection/subscriber_peer_connection.js'); OT.SubscribingState = require('./ot/subscriber/state.js'); OT.VideoDimensionsChangedEvent = Events.VideoDimensionsChangedEvent; OT.VideoDisableWarningEvent = Events.VideoDisableWarningEvent; OT.VideoElement = require('./helpers/video_element/index.js'); OT.VideoEnabledChangedEvent = Events.VideoEnabledChangedEvent; OT.VideoOrientation = require('./helpers/video_orientation.js'); OT.WARN = logging.WARN; OT.WidgetView = require('./helpers/widget_view.js'); OT._ = { getClientGuid: guidStorage.get }; // OT.addEventListener comes from OTHelpers.eventing(OT) OT.analytics = require('./ot/analytics.js'); OT.audioContext = require('./helpers/audio_context.js'); OT.checkScreenSharingCapability = screenSharing.checkCapability; OT.checkSystemRequirements = systemRequirements.check; OT.components = {}; OT.debug = logging.debug; // OT.dispatchEvent comes from OTHelpers.eventing(OT) // OT.emit comes from OTHelpers.eventing(OT) OT.error = logging.error; OT.generateSimpleStateMachine = require('./ot/generate_simple_state_machine.js'); OT.getDevices = require('./ot/get_devices.js'); OT.getErrorTitleByCode = OTError.getTitleByCode; OT.getLogs = logging.getLogs; // This is misspelled in production too, being compatible here. OT.getStatsAdpater = require('./ot/peer_connection/get_stats_adapter.js'); OT.getStatsHelpers = require('./ot/peer_connection/get_stats_helpers.js'); OT.handleJsException = OTError.handleJsException; OT.httpTest = require('./ot/qos_testing/http_test.js'); OT.info = logging.info; OT.initPublisher = require('./ot/publisher/init.js'); OT.initSession = function(apiKey, sessionId) { if (sessionId == null) { sessionId = apiKey; apiKey = null; } // Ugly hack, make sure OT.APIKEY is set // TODO: Yep, sure is ugly. It appears to be needed by raptor. We should fix this situation. // UPDATE: This hack is the only reason why we need to wrap the actual initSession. if (APIKEY.value.length === 0 && apiKey) { APIKEY.value = apiKey; OT.APIKEY = apiKey; } return initSession(apiKey, sessionId); }; OT.isUnloaded = EnvironmentLoader.isUnloaded; OT.log = logging.log; // OT.off comes from OTHelpers.eventing(OT) // OT.on comes from OTHelpers.eventing(OT) OT.onLoad = EnvironmentLoader.onLoad; OT.onUnload = EnvironmentLoader.onUnload; // OT.once comes from OTHelpers.eventing(OT) // Exposed here for partner usage. OT.overrideGuidStorage = guidStorage.override; OT.pickScreenSharingHelper = screenSharing.pickHelper; OT.properties = properties; OT.publishers = sessionObjects.publishers; OT.registerScreenSharingExtension = screenSharing.registerExtension; OT.registerScreenSharingExtensionHelper = screenSharing.registerExtensionHelper; // OT.removeEventListener comes from OTHelpers.eventing(OT) OT.reportIssue = require('./ot/report_issue.js'); OT.sessions = sessionObjects.sessions; OT.setLogLevel = logging.setLogLevel; OT.shouldLog = logging.shouldLog; OT.subscribers = sessionObjects.subscribers; // OT.trigger comes from OTHelpers.eventing(OT) OT.upgradeSystemRequirements = systemRequirements.upgrade; OT.warn = logging.warn; OT.webrtcTest = require('./ot/qos_testing/webrtc_test.js'); // This is here because ../helpers/capabilities mutates OTHelpers. // This is a form of global state. TODO: Avoid global state. // // More information: // The OTHelpers capabilities mechanism is problematic because it requires global state to work. We // intend to make capabilities cleaner by making each one a self-contained module instead of pushing // them into the OTHelpers capabilities registry. // require('./helpers/capabilities'); // Tidy up everything on unload EnvironmentLoader.onUnload(function() { sessionObjects.publishers.destroy(); sessionObjects.subscribers.destroy(); sessionObjects.sessions.destroy('unloaded'); }); if (properties.cssURL) { cssLoader(properties.cssURL); } /* global define */ // Register as a named AMD module, since TokBox could be concatenated with other // files that may use define, but not via a proper concatenation script that // understands anonymous AMD modules. A named AMD is safest and most robust // way to register. Uppercase TB is used because AMD module names are // derived from file names, and OpenTok is normally delivered in an uppercase // file name. if (typeof define === 'function' && define.amd) { define('TB', [], function() { return OT; }); } OT.noConflict = require('./helpers/no_conflict.js')(); module.exports = OT; /** * This method is deprecated. Use on() or once() instead. * ** Registers a method as an event listener for a specific event. *
* *
* The OT object dispatches one type of event an exception event. The
* following code adds an event listener for the exception event:
*
* OT.addEventListener("exception", exceptionHandler);
*
* function exceptionHandler(event) {
* alert("exception event. \n code == " + event.code + "\n message == " + event.message);
* }
*
*
* * If a handler is not registered for an event, the event is ignored locally. If the event * listener function does not exist, the event is ignored locally. *
*
* Throws an exception if the listener name is invalid.
*
* Removes an event listener for a specific event. *
* *
* Throws an exception if the listener name is invalid.
*
* The OT object dispatches one type of event an exception event. The following
* code adds an event
* listener for the exception event:
*
* OT.on("exception", function (event) {
* // This is the event handler.
* });
*
*
* You can also pass in a third context parameter (which is optional) to define the
* value of
* this in the handler method:
* OT.on("exception",
* function (event) {
* // This is the event handler.
* }),
* session
* );
*
*
* * If you do not add a handler for an event, the event is ignored locally. *
* * @param {String} type The string identifying the type of event. * @param {Function} handler The handler function to process the event. This function takes the event * object as a parameter. * @param {Object} context (Optional) Defines the value ofthis in the event handler
* function.
*
* @memberof OT
* @method on
* @see off()
* @see once()
* @see Events
*/
/**
* Adds an event handler function for an event. Once the handler is called, the specified handler
* method is
* removed as a handler for this event. (When you use the OT.on() method to add an event
* handler, the handler
* is not removed when it is called.) The OT.once() method is the equivilent of
* calling the OT.on()
* method and calling OT.off() the first time the handler is invoked.
*
*
* The following code adds a one-time event handler for the exception event:
*
* OT.once("exception", function (event) {
* console.log(event);
* }
*
*
* You can also pass in a third context parameter (which is optional) to define the
* value of
* this in the handler method:
* OT.once("exception",
* function (event) {
* // This is the event handler.
* },
* session
* );
*
*
* * The method also supports an alternate syntax, in which the first parameter is an object that is a * hash map of * event names and handler functions and the second parameter (optional) is the context for this in * each handler: *
*
* OT.once(
* {exeption: function (event) {
* // This is the event handler.
* }
* },
* session
* );
*
*
* @param {String} type The string identifying the type of event. You can specify multiple event
* names in this string,
* separating them with a space. The event handler will process the first occurence of the events.
* After the first event,
* the handler is removed (for all specified events).
* @param {Function} handler The handler function to process the event. This function takes the event
* object as a parameter.
* @param {Object} context (Optional) Defines the value of this in the event handler
* function.
*
* @memberof OT
* @method once
* @see on()
* @see once()
* @see Events
*/
/**
* Removes an event handler.
*
* Pass in an event name and a handler method, the handler is removed for that event:
* *OT.off("exceptionEvent", exceptionEventHandler);
*
* If you pass in an event name and no handler method, all handlers are removed for that * events:
* *OT.off("exceptionEvent");
*
* * The method also supports an alternate syntax, in which the first parameter is an object that is a * hash map of * event names and handler functions and the second parameter (optional) is the context for matching * handlers: *
*
* OT.off(
* {
* exceptionEvent: exceptionEventHandler
* },
* this
* );
*
*
* @param {String} type (Optional) The string identifying the type of event. You can use a space to
* specify multiple events, as in "eventName1 eventName2 eventName3". If you pass in no
* type value (or other arguments), all event handlers are removed for the object.
* @param {Function} handler (Optional) The event handler function to remove. If you pass in no
* handler, all event handlers are removed for the specified event type.
* @param {Object} context (Optional) If you specify a context, the event handler is
* removed for all specified events and handlers that use the specified context.
*
* @memberof OT
* @method off
* @see on()
* @see once()
* @see Events
*/
/**
* Dispatched by the OT class when the app encounters an exception.
* Note that you set up an event handler for the exception event by calling the
* OT.on() method.
*
* @name exception
* @event
* @borrows ExceptionEvent#message as this.message
* @memberof OT
* @see ExceptionEvent
*/
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./helpers/analytics.js":140,"./helpers/audio_context.js":141,"./helpers/audio_level_samplers/getstats_audio_output_level_sampler":142,"./helpers/audio_level_samplers/webaudio_audio_level_sampler":143,"./helpers/capabilities":144,"./helpers/connectivity_attempt_pinger.js":145,"./helpers/css_loader.js":147,"./helpers/dialogs.js":149,"./helpers/get_user_media.js":150,"./helpers/guid_storage.js":151,"./helpers/no_conflict.js":153,"./helpers/properties.js":154,"./helpers/video_element/index.js":159,"./helpers/video_orientation.js":163,"./helpers/widget_view.js":164,"./ot/analytics.js":166,"./ot/anvil.js":167,"./ot/api_key.js":168,"./ot/archive.js":169,"./ot/audio_level_transformer":170,"./ot/capabilities.js":171,"./ot/chrome/chrome.js":176,"./ot/connection.js":180,"./ot/environment_loader.js":181,"./ot/events.js":182,"./ot/exception_codes.js":183,"./ot/generate_simple_state_machine.js":184,"./ot/get_devices.js":185,"./ot/interval_runner.js":186,"./ot/logging.js":187,"./ot/messaging/raptor/legacy_structure.js":191,"./ot/messaging/raptor/session_dispatcher.js":197,"./ot/messaging/raptor/signal.js":198,"./ot/messaging/rumor/legacy_structure.js":201,"./ot/ot_error.js":208,"./ot/peer_connection/get_stats_adapter.js":210,"./ot/peer_connection/get_stats_helpers.js":211,"./ot/peer_connection/peer_connection.js":214,"./ot/peer_connection/peer_connections.js":216,"./ot/peer_connection/publisher_peer_connection.js":218,"./ot/peer_connection/qos.js":219,"./ot/peer_connection/subscriber_peer_connection.js":223,"./ot/publisher":224,"./ot/publisher/init.js":225,"./ot/publisher/microphone.js":227,"./ot/publisher/state.js":228,"./ot/qos_testing/http_test.js":229,"./ot/qos_testing/webrtc_test.js":230,"./ot/report_issue.js":231,"./ot/screensharing/screen_sharing.js":235,"./ot/session/handle.js":236,"./ot/session/info.js":237,"./ot/session/init.js":238,"./ot/session/objects.js":239,"./ot/stream.js":241,"./ot/stream_channel.js":242,"./ot/styling/stylable_component.js":244,"./ot/subscriber":247,"./ot/subscriber/state.js":248,"./ot/system_requirements.js":249,"@opentok/ot-helpers":4}],166:[function(require,module,exports){
'use strict';
var Analytics = require('../helpers/analytics.js');
var properties = require('../helpers/properties.js');
module.exports = new Analytics(properties.loggingURL);
},{"../helpers/analytics.js":140,"../helpers/properties.js":154}],167:[function(require,module,exports){
'use strict';
var uuid = require('uuid');
var Bluebird = require('bluebird');
var ExceptionCodes = require('./exception_codes.js');
var OTHelpers = require('@opentok/ot-helpers');
var properties = require('../helpers/properties.js');
var Anvil = {};
module.exports = Anvil;
// @todo These aren't the same for all resource types.
var httpToClientCode = {
400: ExceptionCodes.INVALID_SESSION_ID,
403: ExceptionCodes.AUTHENTICATION_ERROR,
404: ExceptionCodes.INVALID_SESSION_ID,
409: ExceptionCodes.TERMS_OF_SERVICE_FAILURE
};
var anvilErrors = {
RESPONSE_BADLY_FORMED: {
code: null,
message: 'Unknown error: JSON response was badly formed'
},
UNEXPECTED_SERVER_RESPONSE: {
code: ExceptionCodes.UNEXPECTED_SERVER_RESPONSE,
message: 'Unexpected server response. Try this operation again later.'
}
};
// Transform an error that we don't understand (+originalError+) to a general
// UNEXPECTED_SERVER_RESPONSE one. We also include the original error details
// on the `error.details` property.
//
var normaliseUnexpectedError = function normaliseUnexpectedError(originalError) {
var error = OTHelpers.clone(anvilErrors.UNEXPECTED_SERVER_RESPONSE);
// We don't know this code...capture whatever the original
// error and code were for debugging purposes.
error.details = {
originalCode: originalError.code.toString(),
originalMessage: originalError.message
};
return error;
};
Anvil.getRequestParams = function getApiRequestOptions(resourcePath, token) {
var url = properties.apiURL + '/' + resourcePath;
var options;
var clientVersion = 'js-' + properties.version.replace(/^v/, '');
if (OTHelpers.env.name === 'IE' && OTHelpers.env.version < 10) {
url = url + '&format=json&token=' + encodeURIComponent(token) +
'&version=1&cache=' + uuid() +
'&client_version=' + clientVersion;
options = {
xdomainrequest: true
};
} else {
options = {
headers: {
'X-TB-TOKEN-AUTH': token,
'X-TB-VERSION': 1,
'X-TB-CLIENT-VERSION': clientVersion
}
};
}
return {
url: url,
options: options
};
};
Anvil.getErrorsFromHTTP = function(httpError) {
if (!httpError) {
return false;
}
var error = {
code: httpError.target && httpError.target.status
};
return error;
};
Anvil.getErrorsFromResponse = function getErrorsFromResponse(responseJson) {
if (!Array.isArray(responseJson)) {
return anvilErrors.RESPONSE_BADLY_FORMED;
}
var error = OTHelpers.find(responseJson, function(node) {
return node.error !== void 0 && node.error !== null;
});
if (!error) {
return false;
}
// Yup :-(
error = error.error;
error.message = error.errorMessage && error.errorMessage.message;
return error;
};
var normaliseError = function normaliseError(error, responseText) {
if (error.code && !httpToClientCode[error.code]) {
error = normaliseUnexpectedError(error);
} else {
error.code = httpToClientCode[error.code];
}
if (responseText.length === 0) {
// This is a weird edge case that usually means that there was connectivity
// loss after Anvil sent the response but before the client had fully received it
error.code = ExceptionCodes.CONNECT_FAILED;
responseText = 'Response body was empty, probably due to connectivity loss';
} else if (!error.code) {
error = normaliseUnexpectedError(error);
}
if (!error.details) { error.details = {}; }
error.details.responseText = responseText;
return error;
};
Anvil.get = function getFromAnvil(resourcePath, token) {
var params = Anvil.getRequestParams(resourcePath, token);
return new Bluebird.Promise(function(resolve, reject) {
OTHelpers.getJSON(params.url, params.options, function(httpError, responseJson) {
var err = Anvil.getErrorsFromHTTP(httpError) || Anvil.getErrorsFromResponse(responseJson);
var responseText;
if (err) {
responseText = responseJson && !OTHelpers.isEmpty(responseJson) ?
JSON.stringify(responseJson) : '';
err = normaliseError(err, responseText);
reject(new OTHelpers.Error(err.message, 'AnvilError', {
code: err.code,
details: err.details
}));
return;
}
resolve(responseJson[0]);
});
});
};
},{"../helpers/properties.js":154,"./exception_codes.js":183,"@opentok/ot-helpers":4,"bluebird":70,"uuid":137}],168:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
if (OTHelpers.env.name === 'Node') {
module.exports = { value: '' };
} else {
// Script embed
var scriptSrc = (function() {
var s = document.getElementsByTagName('script');
s = s[s.length - 1];
s = s.getAttribute('src') || s.src;
return s;
})();
var m = scriptSrc.match(/[\?\&]apikey=([^&]+)/i);
// TODO: The indirection here is due to the need to set APIKEY in testing. We should find a better
// solution.
module.exports = { value: m ? m[1] : '' };
}
},{"@opentok/ot-helpers":4}],169:[function(require,module,exports){
'use strict';
var Events = require('./events.js');
var OTHelpers = require('@opentok/ot-helpers');
module.exports = function Archive(id, name, status) {
this.id = id;
this.name = name;
this.status = status;
this._ = {};
OTHelpers.eventing(this);
// Mass update, called by Raptor.Dispatcher
this._.update = function(attributes) {
for (var key in attributes) {
if (!attributes.hasOwnProperty(key)) {
continue;
}
var oldValue = this[key];
this[key] = attributes[key];
var event = new Events.ArchiveUpdatedEvent(this, key, oldValue, this[key]);
this.dispatchEvent(event);
}
}.bind(this);
this.destroy = function() {};
};
},{"./events.js":182,"@opentok/ot-helpers":4}],170:[function(require,module,exports){
'use strict';
/*
* Transforms a raw audio level to produce a "smoother" animation when using displaying the
* audio level. This transformer is state-full because it needs to keep the previous average
* value of the signal for filtering.
*
* It applies a low pass filter to get rid of level jumps and apply a log scale.
*
* @constructor
*/
module.exports = function AudioLevelTransformer() {
var _averageAudioLevel = null;
/*
*
* @param {number} audioLevel a level in the [0,1] range
* @returns {number} a level in the [0,1] range transformed
*/
this.transform = function(audioLevel) {
if (_averageAudioLevel === null || audioLevel >= _averageAudioLevel) {
_averageAudioLevel = audioLevel;
} else {
// a simple low pass filter with a smoothing of 70
_averageAudioLevel = audioLevel * 0.3 + _averageAudioLevel * 0.7;
}
// 1.5 scaling to map -30-0 dBm range to [0,1]
var logScaled = (Math.log(_averageAudioLevel) / Math.LN10) / 1.5 + 1;
return Math.min(Math.max(logScaled, 0), 1);
};
};
},{}],171:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
/**
* A class defining properties of the capabilities property of a
* Session object. See Session.capabilities.
*
* All Capabilities properties are undefined until you have connected to a session
* and the Session object has dispatched the sessionConnected event.
*
* For more information on token roles, see the
* Token Creation Overview.
*
* @class Capabilities
*
* @property {Number} forceDisconnect Specifies whether you can call
* the Session.forceDisconnect() method (1) or not (0). To call the
* Session.forceDisconnect() method,
* the user must have a token that is assigned the role of moderator.
* @property {Number} forceUnpublish Specifies whether you can call
* the Session.forceUnpublish() method (1) or not (0). To call the
* Session.forceUnpublish() method, the user must have a token that
* is assigned the role of moderator.
* @property {Number} publish Specifies whether you can publish to the session (1) or not (0).
* The ability to publish is based on a few factors. To publish, the user must have a token that
* is assigned a role that supports publishing. There must be a connected camera and microphone.
* @property {Number} subscribe Specifies whether you can subscribe to streams
* in the session (1) or not (0). Currently, this capability is available for all users on all
* platforms.
*/
module.exports = function Capabilities(permissions) {
this.publish = permissions.indexOf('publish') !== -1 ? 1 : 0;
this.subscribe = permissions.indexOf('subscribe') !== -1 ? 1 : 0;
this.forceUnpublish = permissions.indexOf('forceunpublish') !== -1 ? 1 : 0;
this.forceDisconnect = permissions.indexOf('forcedisconnect') !== -1 ? 1 : 0;
this.supportsWebRTC = OTHelpers.hasCapabilities('webrtc') ? 1 : 0;
this.permittedTo = function(action) {
return this.hasOwnProperty(action) && this[action] === 1;
};
};
},{"@opentok/ot-helpers":4}],172:[function(require,module,exports){
'use strict';
var Widget = require('./behaviour/widget.js');
var OTHelpers = require('@opentok/ot-helpers');
// Archving Chrome Widget
//
// mode (String)
// Whether to display the archving widget. Possible values are: "on" (the status is displayed
// when archiving and briefly when archving ends) and "off" (the status is not displayed)
// Whether to display the archving widget. Possible values are: "auto" (the name is displayed
// when the status is first displayed and when the user mouses over the display),
// "off" (the name is not displayed), and "on" (the name is displayed).
//
// displays a name
// can be shown/hidden
// can be destroyed
module.exports = function Archiving(options) {
var _lightBox, _light, _text, _textNode, renderStageDelayedAction;
var self = this;
var _archiving = options.archiving;
var _archivingStarted = options.archivingStarted || 'Archiving on';
var _archivingEnded = options.archivingEnded || 'Archiving off';
var _initialState = true;
var renderText = function(text) {
_textNode.nodeValue = text;
_lightBox.setAttribute('title', text);
};
var renderStage = function() {
if (renderStageDelayedAction) {
clearTimeout(renderStageDelayedAction);
renderStageDelayedAction = null;
}
if (_archiving) {
OTHelpers.addClass(_light, 'OT_active');
} else {
OTHelpers.removeClass(_light, 'OT_active');
}
OTHelpers.removeClass(self.domElement, 'OT_archiving-' + (!_archiving ? 'on' : 'off'));
OTHelpers.addClass(self.domElement, 'OT_archiving-' + (_archiving ? 'on' : 'off'));
if (options.show && _archiving) {
renderText(_archivingStarted);
OTHelpers.addClass(_text, 'OT_mode-on');
OTHelpers.removeClass(_text, 'OT_mode-auto');
self.setDisplayMode('on');
renderStageDelayedAction = setTimeout(function() {
OTHelpers.addClass(_text, 'OT_mode-auto');
OTHelpers.removeClass(_text, 'OT_mode-on');
}, 5000);
} else if (options.show && !_initialState) {
OTHelpers.addClass(_text, 'OT_mode-on');
OTHelpers.removeClass(_text, 'OT_mode-auto');
self.setDisplayMode('on');
renderText(_archivingEnded);
renderStageDelayedAction = setTimeout(function() {
self.setDisplayMode('off');
}, 5000);
} else {
self.setDisplayMode('off');
}
};
// Mixin common widget behaviour
Widget(this, {
mode: _archiving && options.show && 'on' || 'off',
nodeName: 'h1',
htmlAttributes: { className: 'OT_archiving OT_edge-bar-item OT_edge-bottom' },
onCreate: function() {
_lightBox = OTHelpers.createElement('div', {
className: 'OT_archiving-light-box'
}, '');
_light = OTHelpers.createElement('div', {
className: 'OT_archiving-light'
}, '');
_lightBox.appendChild(_light);
_text = OTHelpers.createElement('div', {
className: 'OT_archiving-status OT_mode-on OT_edge-bar-item OT_edge-bottom'
}, '');
_textNode = document.createTextNode('');
_text.appendChild(_textNode);
self.domElement.appendChild(_lightBox);
self.domElement.appendChild(_text);
renderStage();
}
});
this.setShowArchiveStatus = function(show) {
options.show = show;
if (self.domElement) {
renderStage.call(self);
}
};
this.setArchiving = function(status) {
_archiving = status;
_initialState = false;
if (self.domElement) {
renderStage.call(self);
}
};
};
},{"./behaviour/widget.js":175,"@opentok/ot-helpers":4}],173:[function(require,module,exports){
'use strict';
var Widget = require('./behaviour/widget');
var OTHelpers = require('@opentok/ot-helpers');
var Promise = require('bluebird');
var EventEmitter = require('events');
module.exports = function AudioLevelMeter(options) {
var _audioLevelMeter = this;
var _eventEmitter = new EventEmitter();
var _meterBarElement,
_voiceOnlyIconElement,
_meterValueElement,
_value,
_lastComputedVisibility;
// display the widget by default but can be hidden when calling hideWhileLoading
var _displayAroundLoading = true;
var _audioOnly = false;
var _displayMode = 'auto';
var _maxValue = options.maxValue || 1;
var _minValue = options.minValue || 0;
function onCreate() {
_meterBarElement = OTHelpers.createElement('div', {
className: 'OT_audio-level-meter__bar'
}, '');
_meterValueElement = OTHelpers.createElement('div', {
className: 'OT_audio-level-meter__value'
}, '');
_voiceOnlyIconElement = OTHelpers.createElement('div', {
className: 'OT_audio-level-meter__audio-only-img'
}, '');
var domElement = _audioLevelMeter.domElement;
domElement.appendChild(_meterBarElement);
domElement.appendChild(_voiceOnlyIconElement);
domElement.appendChild(_meterValueElement);
_audioLevelMeter.watchVisibilityChanged(function(visible) {
if (visible) {
OTHelpers.removeClass(_audioLevelMeter.domElement, 'OT_hide-forced');
} else {
OTHelpers.addClass(_audioLevelMeter.domElement, 'OT_hide-forced');
}
});
}
function onDestroy() {
_eventEmitter.removeAllListeners('visibilityChanged');
}
function updateView() {
var percentSize = _value * 100 / (_maxValue - _minValue);
_meterValueElement.style.width = _meterValueElement.style.height = 2 * percentSize + '%';
_meterValueElement.style.top = _meterValueElement.style.right = -percentSize + '%';
}
// computes the visibility value from the different "inputs" variables and asynchronously triggers
// the internal "visibilityChanged" events if the value changed from last time it was computed
function computeVisibility() {
var computedVisibility = (_audioOnly && _displayMode === 'auto' || _displayMode === 'on')
&& _displayAroundLoading;
if (_lastComputedVisibility !== computedVisibility) {
_lastComputedVisibility = computedVisibility;
_eventEmitter.emit('visibilityChanged', computedVisibility);
}
}
OTHelpers.defineProperties(_audioLevelMeter, {
audioOnly: {
get: function() { return _audioOnly; },
set: function(audioOnly) {
_audioOnly = audioOnly;
computeVisibility();
}
}
});
_audioLevelMeter.setValue = function(value) {
_value = value;
updateView();
};
/**
* Registers an callback to be executed when the visibility of the audio level meter changes.
* "true" means the widget is shown.
* The contracts is that the handlers should not try to change the visibility of the widget by
* changing the value of visibility "inputs" (setting "audioOnly", "displayMode" or calling
* "hideWhileLoading" and "showAfterLoading")
*
* @param {function(boolean)} cb the callback to be executed when the display value changes.
* The callback is also executed with the last computed value when registered.
* @returns {function} a callback to unregister the handler
*/
_audioLevelMeter.watchVisibilityChanged = function(cb) {
_eventEmitter.on('visibilityChanged', cb);
Promise.resolve().then(function() {
cb(_lastComputedVisibility);
});
return function stopWatching() {
_eventEmitter.removeListener('visibilityChanged', cb);
};
};
// Mixin common widget behaviour
var widgetOptions = {
mode: options ? options.mode : 'auto',
nodeName: 'div',
htmlAttributes: {
className: 'OT_audio-level-meter'
},
onCreate: onCreate,
onDestroy: onDestroy
};
Widget(this, widgetOptions);
// The methods underneath are mixed in by "Widget" but overridden
// Doing so, we can bypass it and compute the display value ourselves without relying on CSS
_audioLevelMeter.setDisplayMode = function(mode) {
_displayMode = mode;
computeVisibility();
};
_audioLevelMeter.getDisplayMode = function() {
return _displayMode;
};
_audioLevelMeter.showAfterLoading = function() {
_displayAroundLoading = true;
computeVisibility();
};
_audioLevelMeter.hideWhileLoading = function() {
_displayAroundLoading = false;
computeVisibility();
};
// compute the initial visibility value
computeVisibility();
};
},{"./behaviour/widget":175,"@opentok/ot-helpers":4,"bluebird":70,"events":72}],174:[function(require,module,exports){
'use strict';
var Widget = require('./behaviour/widget.js');
// BackingBar Chrome Widget
//
// nameMode (String)
// Whether or not the name panel is being displayed
// Possible values are: "auto" (the name is displayed
// when the stream is first displayed and when the user mouses over the display),
// "off" (the name is not displayed), and "on" (the name is displayed).
//
// muteMode (String)
// Whether or not the mute button is being displayed
// Possible values are: "auto" (the mute button is displayed
// when the stream is first displayed and when the user mouses over the display),
// "off" (the mute button is not displayed), and "on" (the mute button is displayed).
//
// displays a backing bar
// can be shown/hidden
// can be destroyed
module.exports = function BackingBar(options) {
var _nameMode = options.nameMode;
var _muteMode = options.muteMode;
function getDisplayMode() {
if (_nameMode === 'on' || _muteMode === 'on') {
return 'on';
} else if (_nameMode === 'mini' || _muteMode === 'mini') {
return 'mini';
} else if (_nameMode === 'mini-auto' || _muteMode === 'mini-auto') {
return 'mini-auto';
} else if (_nameMode === 'auto' || _muteMode === 'auto') {
return 'auto';
}
return 'off';
}
// Mixin common widget behaviour
Widget(this, {
mode: getDisplayMode(),
nodeName: 'div',
htmlContent: '',
htmlAttributes: {
className: 'OT_bar OT_edge-bar-item'
}
});
this.setNameMode = function(nameMode) {
_nameMode = nameMode;
this.setDisplayMode(getDisplayMode());
};
this.setMuteMode = function(muteMode) {
_muteMode = muteMode;
this.setDisplayMode(getDisplayMode());
};
};
},{"./behaviour/widget.js":175}],175:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
// A mixin to encapsulate the basic widget behaviour. This needs a better name,
// it's not actually a widget. It's actually "Behaviour that can be applied to
// an object to make it support the basic Chrome widget workflow"...but that would
// probably been too long a name.
module.exports = function Widget(widget, options) {
var _mode;
var _options = options || {};
//
// @param [String] mode
// 'on', 'off', or 'auto'
//
widget.setDisplayMode = function(mode) {
var newMode = mode || 'auto';
if (_mode === newMode) { return; }
OTHelpers.removeClass(this.domElement, 'OT_mode-' + _mode);
OTHelpers.addClass(this.domElement, 'OT_mode-' + newMode);
_mode = newMode;
};
widget.getDisplayMode = function() {
return _mode;
};
widget.showAfterLoading = function() {
OTHelpers.removeClass(this.domElement, 'OT_hide-forced');
};
widget.hideWhileLoading = function() {
OTHelpers.addClass(this.domElement, 'OT_hide-forced');
};
widget.destroy = function() {
if (_options.onDestroy) { _options.onDestroy(this.domElement); }
if (this.domElement) { OTHelpers.removeElement(this.domElement); }
return widget;
};
widget.appendTo = function(parent) {
// create the element under parent
this.domElement = OTHelpers.createElement(_options.nodeName || 'div',
_options.htmlAttributes,
_options.htmlContent);
if (_options.onCreate) { _options.onCreate(this.domElement); }
widget.setDisplayMode(_options.mode);
if (_options.mode === 'auto') {
// if the mode is auto we hold the "on mode" for 2 seconds
// this will let the proper widgets nicely fade away and help discoverability
OTHelpers.addClass(widget.domElement, 'OT_mode-on-hold');
setTimeout(function() {
OTHelpers.removeClass(widget.domElement, 'OT_mode-on-hold');
}, 2000);
}
// add the widget to the parent
parent.appendChild(this.domElement);
return widget;
};
};
},{"@opentok/ot-helpers":4}],176:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
// Manages N Chrome elements
module.exports = function Chrome(properties) {
var _widgets = {};
// Private helper function
var _set = function(name, widget) {
widget.parent = this;
widget.appendTo(properties.parent);
_widgets[name] = widget;
this[name] = widget;
};
if (!properties.parent) {
// @todo raise an exception
return;
}
OTHelpers.eventing(this);
this.destroy = function() {
this.off();
this.hideWhileLoading();
for (var name in _widgets) {
if (_widgets.hasOwnProperty(name)) {
_widgets[name].destroy();
}
}
};
this.showAfterLoading = function() {
for (var name in _widgets) {
if (_widgets.hasOwnProperty(name)) {
_widgets[name].showAfterLoading();
}
}
};
this.hideWhileLoading = function() {
for (var name in _widgets) {
if (_widgets.hasOwnProperty(name)) {
_widgets[name].hideWhileLoading();
}
}
};
// Adds the widget to the chrome and to the DOM. Also creates a accessor
// property for it on the chrome.
//
// @example
// chrome.set('foo', new FooWidget());
// chrome.foo.setDisplayMode('on');
//
// @example
// chrome.set({
// foo: new FooWidget(),
// bar: new BarWidget()
// });
// chrome.foo.setDisplayMode('on');
//
this.set = function(widgetName, widget) {
if (typeof widgetName === 'string' && widget) {
_set.call(this, widgetName, widget);
} else {
for (var name in widgetName) {
if (widgetName.hasOwnProperty(name)) {
_set.call(this, name, widgetName[name]);
}
}
}
return this;
};
};
},{"@opentok/ot-helpers":4}],177:[function(require,module,exports){
'use strict';
var Widget = require('./behaviour/widget.js');
var OTHelpers = require('@opentok/ot-helpers');
module.exports = function MuteButton(options) {
var _onClickCb;
var _muted = options.muted || false;
var updateClasses = function() {
if (_muted) {
OTHelpers.addClass(this.domElement, 'OT_active');
} else {
OTHelpers.removeClass(this.domElement, 'OT_active ');
}
}.bind(this);
// Private Event Callbacks
var attachEvents = function(elem) {
_onClickCb = onClick.bind(this);
OTHelpers.on(elem, 'click', _onClickCb);
};
var detachEvents = function(elem) {
_onClickCb = null;
OTHelpers.off(elem, 'click', _onClickCb);
};
var onClick = function() {
_muted = !_muted;
updateClasses();
if (_muted) {
this.parent.trigger('muted', this);
} else {
this.parent.trigger('unmuted', this);
}
return false;
};
OTHelpers.defineProperties(this, {
muted: {
get: function() { return _muted; },
set: function(muted) {
_muted = muted;
updateClasses();
}
}
});
// Mixin common widget behaviour
var classNames = _muted ? 'OT_edge-bar-item OT_mute OT_active' : 'OT_edge-bar-item OT_mute';
Widget(this, {
mode: options.mode,
nodeName: 'button',
htmlContent: 'Mute',
htmlAttributes: {
className: classNames
},
onCreate: attachEvents.bind(this),
onDestroy: detachEvents.bind(this)
});
};
},{"./behaviour/widget.js":175,"@opentok/ot-helpers":4}],178:[function(require,module,exports){
'use strict';
var Widget = require('./behaviour/widget.js');
// NamePanel Chrome Widget
//
// mode (String)
// Whether to display the name. Possible values are: "auto" (the name is displayed
// when the stream is first displayed and when the user mouses over the display),
// "off" (the name is not displayed), and "on" (the name is displayed).
//
// displays a name
// can be shown/hidden
// can be destroyed
module.exports = function NamePanel(options) {
var _name = options.name;
if (!_name || _name.trim().length === '') {
_name = null;
// THere's no name, just flip the mode off
options.mode = 'off';
}
this.setName = function(name) {
if (!_name) { this.setDisplayMode('auto'); }
_name = name;
this.domElement.innerHTML = _name;
}.bind(this);
// Mixin common widget behaviour
Widget(this, {
mode: options.mode,
nodeName: 'h1',
htmlContent: _name,
htmlAttributes: {
className: 'OT_name OT_edge-bar-item'
}
});
};
},{"./behaviour/widget.js":175}],179:[function(require,module,exports){
'use strict';
var Widget = require('./behaviour/widget.js');
var OTHelpers = require('@opentok/ot-helpers');
module.exports = function VideoDisabledIndicator(options) {
var videoDisabled = false;
var warning = false;
var updateClasses = function(element) {
var shouldDisplay = ['auto', 'on'].indexOf(this.getDisplayMode()) > -1;
OTHelpers.removeClass(element, 'OT_video-disabled OT_video-disabled-warning OT_active');
if (!shouldDisplay) {
return;
}
if (videoDisabled) {
OTHelpers.addClass(element, 'OT_video-disabled');
} else if (warning) {
OTHelpers.addClass(element, 'OT_video-disabled-warning');
}
OTHelpers.addClass(element, 'OT_active');
}.bind(this);
this.disableVideo = function(value) {
videoDisabled = value;
if (value === true) {
warning = false;
}
updateClasses(this.domElement);
};
this.setWarning = function(value) {
warning = value;
updateClasses(this.domElement);
};
// Mixin common widget behaviour
Widget(this, {
mode: options.mode || 'auto',
nodeName: 'div',
htmlAttributes: {
className: 'OT_video-disabled-indicator'
}
});
var parentSetDisplayMode = this.setDisplayMode.bind(this);
this.setDisplayMode = function(mode) {
parentSetDisplayMode(mode);
updateClasses(this.domElement);
};
};
},{"./behaviour/widget.js":175,"@opentok/ot-helpers":4}],180:[function(require,module,exports){
'use strict';
var Capabilities = require('./capabilities.js');
var Events = require('./events.js');
var OTHelpers = require('@opentok/ot-helpers');
/**
* The Connection object represents a connection to an OpenTok session. Each client that connects
* to a session has a unique connection, with a unique connection ID (represented by the
* id property of the Connection object for the client).
*
* The Session object has a connection property that is a Connection object.
* It represents the local client's connection. (A client only has a connection once the
* client has successfully called the connect() method of the {@link Session}
* object.)
*
* The Session object dispatches a connectionCreated event when each client (including
* your own) connects to a session (and for clients that are present in the session when you
* connect). The connectionCreated event object has a connection
* property, which is a Connection object corresponding to the client the event pertains to.
*
* The Stream object has a connection property that is a Connection object.
* It represents the connection of the client that is publishing the stream.
*
* @class Connection
* @property {String} connectionId The ID of this connection.
* @property {Number} creationTime The timestamp for the creation of the connection. This
* value is calculated in milliseconds.
* You can convert this value to a Date object by calling new Date(creationTime),
* where creationTime
* is the creationTime property of the Connection object.
* @property {String} data A string containing metadata describing the
* connection. When you generate a user token string pass the connection data string to the
* generate_token() method of our
* server-side libraries. You can
* also generate a token and define connection data on the
* Dashboard page.
*/
var Connection = function(id, creationTime, data, capabilitiesHash, permissionsHash) {
var destroyedReason;
this.id = this.connectionId = id;
this.creationTime = creationTime ? Number(creationTime) : null;
this.data = data;
this.capabilities = new Connection.Capabilities(capabilitiesHash);
this.permissions = new Capabilities(permissionsHash);
this.quality = null;
OTHelpers.eventing(this);
this.destroy = function(reason, quiet) {
destroyedReason = reason || 'clientDisconnected';
if (quiet !== true) {
this.dispatchEvent(
new Events.DestroyedEvent(
// This should be Events.Event.names.CONNECTION_DESTROYED, but
// the value of that is currently shared with Session
'destroyed',
this,
destroyedReason
)
);
}
}.bind(this);
this.destroyed = function() {
return destroyedReason !== void 0;
};
this.destroyedReason = function() {
return destroyedReason;
};
};
Connection.fromHash = function(hash) {
return new Connection(
hash.id,
hash.creationTime,
hash.data,
OTHelpers.extend(hash.capablities || {}, { supportsWebRTC: true }),
hash.permissions || []
);
};
// TODO: This is weird and feels like it shouldn't exist.
Connection.Capabilities = function(capabilitiesHash) {
// Private helper methods
var castCapabilities = function(capabilitiesHash) {
capabilitiesHash.supportsWebRTC = OTHelpers.castToBoolean(capabilitiesHash.supportsWebRTC);
return capabilitiesHash;
};
// Private data
var _caps = castCapabilities(capabilitiesHash);
this.supportsWebRTC = _caps.supportsWebRTC;
};
module.exports = Connection;
},{"./capabilities.js":171,"./events.js":182,"@opentok/ot-helpers":4}],181:[function(require,module,exports){
'use strict';
var Events = require('./events.js');
var OTPlugin = require('@opentok/otplugin.js');
var OTHelpers = require('@opentok/ot-helpers');
var logging = require('./logging.js');
// Helper to synchronise several startup tasks and then dispatch a unified
// 'envLoaded' event.
function EnvironmentLoader() {
var environmentLoader = this;
OTHelpers.eventing(environmentLoader, true);
// If the plugin is installed, then we should wait for it to be ready as well.
var _pluginSupported = OTPlugin.isSupported();
var _pluginLoadAttemptComplete = _pluginSupported ? OTPlugin.isReady() : true;
var isReady = function() {
return !OTHelpers.isDOMUnloaded() &&
OTHelpers.isReady() &&
_pluginLoadAttemptComplete;
};
var onLoaded = function() {
if (isReady()) {
environmentLoader.dispatchEvent(
new Events.EnvLoadedEvent(Events.Event.names.ENV_LOADED)
);
}
};
var onDomReady = function() {
OTHelpers.onDOMUnload(onDomUnload);
onLoaded();
};
var onDomUnload = function() {
environmentLoader.dispatchEvent(
new Events.EnvLoadedEvent(Events.Event.names.ENV_UNLOADED)
);
};
var onPluginReady = function(err) {
// We mark the plugin as ready so as not to stall the environment
// loader. In this case though, OTPlugin is not supported.
_pluginLoadAttemptComplete = true;
if (err) {
logging.error('OTPlugin failed to load or was not installed:', err);
}
onLoaded();
};
OTHelpers.onDOMLoad(onDomReady);
// If the plugin should work on this platform then
// see if it loads.
if (_pluginSupported) { OTPlugin.ready(onPluginReady); }
this.onLoad = function(cb, context) {
if (isReady()) {
cb.call(context);
return;
}
environmentLoader.on(Events.Event.names.ENV_LOADED, cb, context);
};
this.onUnload = function(cb, context) {
if (this.isUnloaded()) {
cb.call(context);
return;
}
environmentLoader.on(Events.Event.names.ENV_UNLOADED, cb, context);
};
this.isUnloaded = function() {
return OTHelpers.isDOMUnloaded();
};
}
module.exports = new EnvironmentLoader();
},{"./events.js":182,"./logging.js":187,"@opentok/ot-helpers":4,"@opentok/otplugin.js":40}],182:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
var logging = require('./logging.js');
var Events = {};
/**
* The Event object defines the basic OpenTok event object that is passed to
* event listeners. Other OpenTok event classes implement the properties and methods of
* the Event object.
For example, the Stream object dispatches a streamPropertyChanged event when
* the stream's properties are updated. You add a callback for an event using the
* on() method of the Stream object:
* stream.on("streamPropertyChanged", function (event) {
* alert("Properties changed for stream " + event.target.streamId);
* });
*
* @class Event
* @property {Boolean} cancelable Whether the event has a default behavior that is cancelable
* (true) or not (false). You can cancel the default behavior by
* calling the preventDefault() method of the Event object in the callback
* function. (See preventDefault().)
*
* @property {Object} target The object that dispatched the event.
*
* @property {String} type The type of event.
*/
Events.Event = OTHelpers.Event();
/**
* Prevents the default behavior associated with the event from taking place.
*
* To see whether an event has a default behavior, check the cancelable property
* of the event object.
Call the preventDefault() method in the callback function for the event.
The following events have default behaviors:
* *sessionDisconnect See
*
* SessionDisconnectEvent.preventDefault().streamDestroyed See
* StreamEvent.preventDefault().accessDialogOpened See the
* accessDialogOpened event.accessDenied See the
* accessDenied event.preventDefault() (true) or not (false).
* See preventDefault().
* @method #isDefaultPrevented
* @return {Boolean}
* @memberof Event
*/
// Event names lookup
Events.Event.names = {
// Activity Status for cams/mics
ACTIVE: 'active',
INACTIVE: 'inactive',
UNKNOWN: 'unknown',
// Archive types
PER_SESSION: 'perSession',
PER_STREAM: 'perStream',
// Events
EXCEPTION: 'exception',
ISSUE_REPORTED: 'issueReported',
// Session Events
SESSION_CONNECTED: 'sessionConnected',
SESSION_RECONNECTING: 'sessionReconnecting',
SESSION_RECONNECTED: 'sessionReconnected',
SESSION_DISCONNECTED: 'sessionDisconnected',
STREAM_CREATED: 'streamCreated',
STREAM_DESTROYED: 'streamDestroyed',
CONNECTION_CREATED: 'connectionCreated',
CONNECTION_DESTROYED: 'connectionDestroyed',
SIGNAL: 'signal',
STREAM_PROPERTY_CHANGED: 'streamPropertyChanged',
MICROPHONE_LEVEL_CHANGED: 'microphoneLevelChanged',
// Publisher Events
RESIZE: 'resize',
SETTINGS_BUTTON_CLICK: 'settingsButtonClick',
DEVICE_INACTIVE: 'deviceInactive',
INVALID_DEVICE_NAME: 'invalidDeviceName',
ACCESS_ALLOWED: 'accessAllowed',
ACCESS_DENIED: 'accessDenied',
ACCESS_DIALOG_OPENED: 'accessDialogOpened',
ACCESS_DIALOG_CLOSED: 'accessDialogClosed',
ECHO_CANCELLATION_MODE_CHANGED: 'echoCancellationModeChanged',
MEDIA_STOPPED: 'mediaStopped',
PUBLISHER_DESTROYED: 'destroyed',
// Subscriber Events
SUBSCRIBER_DESTROYED: 'destroyed',
SUBSCRIBER_CONNECTED: 'connected',
SUBSCRIBER_DISCONNECTED: 'disconnected',
// DeviceManager Events
DEVICES_DETECTED: 'devicesDetected',
// DevicePanel Events
DEVICES_SELECTED: 'devicesSelected',
CLOSE_BUTTON_CLICK: 'closeButtonClick',
MICLEVEL: 'microphoneActivityLevel',
MICGAINCHANGED: 'microphoneGainChanged',
// Environment Loader
ENV_LOADED: 'envLoaded',
ENV_UNLOADED: 'envUnloaded',
// Audio activity Events
AUDIO_LEVEL_UPDATED: 'audioLevelUpdated',
VIDEO_ELEMENT_CREATED: 'videoElementCreated'
};
/**
* The {@link OT} class dispatches exception events when the OpenTok API encounters
* an exception (error). The ExceptionEvent object defines the properties of the event
* object that is dispatched.
*
* Note that you set up a callback for the exception event by calling the
* OT.on() method.
| * code * * | ** title * | *
| * 1004 * * | ** Authentication error * | *
| * 1005 * * | ** Invalid Session ID * | *
| * 1006 * * | ** Connect Failed * | *
| * 1007 * * | ** Connect Rejected * | *
| * 1008 * * | ** Connect Time-out * | *
| * 1009 * * | ** Security Error * | *
| * 1010 * * | ** Not Connected * | *
| * 1011 * * | ** Invalid Parameter * | *
| * 1013 * | ** Connection Failed * | *
| * 1014 * | ** API Response Failure * | *
| * 1026 * | ** Terms of Service Violation: Export Compliance * | *
| * 1500 * | ** Unable to Publish * | *
| * 1520 * | ** Unable to Force Disconnect * | *
| * 1530 * | ** Unable to Force Unpublish * | *
| * 1535 * | ** Force Unpublish on Invalid Stream * | *
| * 2000 * * | ** Internal Error * | *
| * 2010 * * | ** Report Issue Failure * | *
Check the message property for more details about the error.
exception event, this will be an object other than the OT object
* (such as a Session object or a Publisher object).
*
* @property {String} title The error title.
* @augments Event
*/
Events.ExceptionEvent = function(type, message, title, code, component, target) {
Events.Event.call(this, type);
this.message = message;
this.title = title;
this.code = code;
this.component = component;
this.target = target;
};
Events.IssueReportedEvent = function(type, issueId) {
Events.Event.call(this, type);
this.issueId = issueId;
};
// Triggered when the JS dynamic config and the DOM have loaded.
Events.EnvLoadedEvent = function(type) {
Events.Event.call(this, type);
};
/**
* Defines connectionCreated and connectionDestroyed events dispatched
* by the {@link Session} object.
*
* The Session object dispatches a connectionCreated event when a client (including
* your own) connects to a Session. It also dispatches a connectionCreated event for
* every client in the session when you first connect. (when your local client connects, the
* Session object also dispatches a sessionConnected event, defined by the
* {@link SessionConnectEvent} class.)
*
* While you are connected to the session, the Session object dispatches a
* connectionDestroyed event when another client disconnects from the Session.
* (When you disconnect, the Session object also dispatches a sessionDisconnected
* event, defined by the {@link SessionDisconnectEvent} class.)
*
*
The following code keeps a running total of the number of connections to a session
* by monitoring the connections property of the sessionConnect,
* connectionCreated and connectionDestroyed events:
var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
* var sessionID = ""; // Replace with your own session ID.
* // See https://dashboard.tokbox.com/projects
* var token = ""; // Replace with a generated token that has been assigned the moderator role.
* // See https://dashboard.tokbox.com/projects
* var connectionCount = 0;
*
* var session = OT.initSession(apiKey, sessionID);
* session.on("connectionCreated", function(event) {
* connectionCount++;
* displayConnectionCount();
* });
* session.on("connectionDestroyed", function(event) {
* connectionCount--;
* displayConnectionCount();
* });
* session.connect(token);
*
* function displayConnectionCount() {
* document.getElementById("connectionCountField").value = connectionCount.toString();
* }
*
* This example assumes that there is an input text field in the HTML DOM
* with the id set to "connectionCountField":
<input type="text" id="connectionCountField" value="0"></input>* * * @property {Connection} connection A Connection objects for the connections that was * created or deleted. * * @property {Array} connections Deprecated. Use the
connection property. A
* connectionCreated or connectionDestroyed event is dispatched
* for each connection created and destroyed in the session.
*
* @property {String} reason For a connectionDestroyed event,
* a description of why the connection ended. This property can have two values:
*
* "clientDisconnected" A client disconnected from the session by calling
* the disconnect() method of the Session object or by closing the browser.
* (See Session.disconnect().)"forceDisconnected" A moderator has disconnected the publisher
* from the session, by calling the forceDisconnect() method of the Session
* object. (See Session.forceDisconnect().)"networkDisconnected" The network connection terminated abruptly
* (for example, the client lost their internet connection).Depending on the context, this description may allow the developer to refine * the course of action they take in response to an event.
* *For a connectionCreated event, this string is undefined.
The following code initializes a session and sets up an event listener for when * a stream published by another client is created:
* *
* session.on("streamCreated", function(event) {
* // streamContainer is a DOM element
* subscriber = session.subscribe(event.stream, targetElement);
* }).connect(token);
*
*
* The following code initializes a session and sets up an event listener for when * other clients' streams end:
* *
* session.on("streamDestroyed", function(event) {
* console.log("Stream " + event.stream.name + " ended. " + event.reason);
* }).connect(token);
*
*
* The following code publishes a stream and adds an event listener for when the streaming * starts
* *
* var publisher = session.publish(targetElement)
* .on("streamCreated", function(event) {
* console.log("Publisher started streaming.");
* );
*
*
* The following code publishes a stream, and leaves the Publisher in the HTML DOM * when the streaming stops:
* *
* var publisher = session.publish(targetElement)
* .on("streamDestroyed", function(event) {
* event.preventDefault();
* console.log("Publisher stopped streaming.");
* );
*
*
* @class StreamEvent
*
* @property {Boolean} cancelable Whether the event has a default behavior that is cancelable
* (true) or not (false). You can cancel the default behavior by
* calling the preventDefault() method of the StreamEvent object in the event
* listener function. The streamDestroyed event is cancelable.
* (See preventDefault().)
*
* @property {String} reason For a streamDestroyed event,
* a description of why the session disconnected. This property can have one of the following
* values:
*
* "clientDisconnected" A client disconnected from the session by calling
* the disconnect() method of the Session object or by closing the browser.
* (See Session.disconnect().)"forceDisconnected" A moderator has disconnected the publisher of the
* stream from the session, by calling the forceDisconnect() method of the Session
* object. (See Session.forceDisconnect().)"forceUnpublished" A moderator has forced the publisher of the stream
* to stop publishing the stream, by calling the forceUnpublish() method of the
* Session object.
* (See Session.forceUnpublish().)"mediaStopped" The user publishing the stream has stopped sharing the
* screen. This value is only used in screen-sharing video streams."networkDisconnected" The network connection terminated abruptly (for
* example, the client lost their internet connection).Depending on the context, this description may allow the developer to refine * the course of action they take in response to an event.
* *For a streamCreated event, this string is undefined.
streamCreated event) or deleted (in the case of a
* streamDestroyed event).
*
* @property {Array} streams Deprecated. Use the stream property. A
* streamCreated or streamDestroyed event is dispatched for
* each stream added or destroyed.
*
* @augments Event
*/
var streamEventPluralDeprecationWarningShown = false;
Events.StreamEvent = function(type, stream, reason, cancelable) {
Events.Event.call(this, type, cancelable);
Object.defineProperty(this, 'streams', {
get: function() {
if (!streamEventPluralDeprecationWarningShown) {
logging.warn('OT.StreamEvent streams property is deprecated, use stream instead.');
streamEventPluralDeprecationWarningShown = true;
}
return [stream];
}
});
this.stream = stream;
this.reason = reason;
};
/**
* Prevents the default behavior associated with the event from taking place.
*
* For the streamDestroyed event dispatched by the Session object,
* the default behavior is that all Subscriber objects that are subscribed to the stream are
* unsubscribed and removed from the HTML DOM. Each Subscriber object dispatches a
* destroyed event when the element is removed from the HTML DOM. If you call the
* preventDefault() method in the event listener for the streamDestroyed
* event, the default behavior is prevented and you can clean up Subscriber objects using your
* own code. See
* Session.getSubscribersForStream().
* For the streamDestroyed event dispatched by a Publisher object, the default
* behavior is that the Publisher object is removed from the HTML DOM. The Publisher object
* dispatches a destroyed event when the element is removed from the HTML DOM.
* If you call the preventDefault() method in the event listener for the
* streamDestroyed event, the default behavior is prevented, and you can
* retain the Publisher for reuse or clean it up using your own code.
*
To see whether an event has a default behavior, check the cancelable property of
* the event object.
* Call the preventDefault() method in the event listener function for the event.
*
connect() method of the Session object.
*
* In version 2.2, the completionHandler of the Session.connect() method
* indicates success or failure in connecting to the session.
*
* @class SessionConnectEvent
* @property {Array} connections Deprecated in version 2.2 (and set to an empty array). In
* version 2.2, listen for the connectionCreated event dispatched by the Session
* object. In version 2.2, the Session object dispatches a connectionCreated event
* for each connection (including your own). This includes connections present when you first
* connect to the session.
*
* @property {Array} streams Deprecated in version 2.2 (and set to an empty array). In version
* 2.2, listen for the streamCreated event dispatched by the Session object. In
* version 2.2, the Session object dispatches a streamCreated event for each stream
* other than those published by your client. This includes streams
* present when you first connect to the session.
*
* @see Session.connect()
disconnect() method of the session object.
*
* * The following code initializes a session and sets up an event listener for when a session is * disconnected. *
*var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
* var sessionID = ""; // Replace with your own session ID.
* // See https://dashboard.tokbox.com/projects
* var token = ""; // Replace with a generated token that has been assigned the moderator role.
* // See https://dashboard.tokbox.com/projects
*
* var session = OT.initSession(apiKey, sessionID);
* session.on("sessionDisconnected", function(event) {
* alert("The session disconnected. " + event.reason);
* });
* session.connect(token);
*
*
* @property {String} reason A description of why the session disconnected.
* This property can have two values:
*
* "clientDisconnected" — A client disconnected from the
* session by calling the disconnect() method of the Session
* object or by closing the browser. ( See Session.disconnect().)
* "forceDisconnected" — A moderator has disconnected you from
* the session by calling the forceDisconnect() method of the
* Session object. (See Session.forceDisconnect().)
* "networkDisconnected" — The network connection terminated
* abruptly (for example, the client lost its internet connection).
*
* If you are participating in the automatic reconnection beta program, prior to dispatching a
* sessionDisconnected event for this reason, the Session object dispatches a
* reconnecting event, and the client attempts to reconnect to the OpenTok session.
* If the reconnection fails, the Session object dispatches a sessionDisconnected
* event with the reason property set to "networkDisconnected".
* To participate in the automatic reconnection beta program, see the
* OpenTok Beta programs page.
*
* For the sessionDisconnectEvent, the default behavior is that all
* Subscriber objects are unsubscribed and removed from the HTML DOM. Each
* Subscriber object dispatches a destroyed event when the element
* is removed from the HTML DOM. If you call the preventDefault()
* method in the event listener for the sessionDisconnect event,
* the default behavior is prevented, and you can, optionally, clean up
* Subscriber objects using your own code). *
*
* To see whether an event has a default behavior, check the
* cancelable property of the event object.
*
* Call the preventDefault() method in the event listener function
* for the event.
*
streamPropertyChanged event in the
* following circumstances:
*
* publishAudio() or
* publishVideo() methods of the Publish object. Note that a
* subscriber's video can be disabled or enabled for reasons other than the
* publisher disabling or enabling it. A Subscriber object dispatches
* videoDisabled and videoEnabled events in all
* conditions that cause the subscriber's stream to be disabled or enabled.
* videoDimensions property of the Stream object has
* changed (see Stream.videoDimensions).
* videoType property of the Stream object has changed.
* This can happen in a stream published by a mobile device. (See
* Stream.videoType.)
* "hasAudio", "hasVideo", or "videoDimensions".
* @property {Object} newValue The new value of the property (after the change).
* @property {Object} oldValue The old value of the property (before the change).
* @property {Stream} stream The Stream object for which a property has changed.
*
* @see Publisher.publishAudio()
* @see Publisher.publishVideo()
* @see Stream.videoDimensions
* @augments Event
*/
Events.StreamPropertyChangedEvent = function(type, stream, changedProperty, oldValue, newValue) {
Events.Event.call(this, type, false);
this.type = type;
this.stream = stream;
this.changedProperty = changedProperty;
this.oldValue = oldValue;
this.newValue = newValue;
};
/**
* Dispatched when the video dimensions of the video change for a screen-sharing
* video stream (when the user resizes the window being captured).
*
* @class VideoDimensionsChangedEvent
* @property {Object} newValue The new video dimensions (after the change). This object has two
* properties: height (the height, in pixels) and width (the width,
* in pixels).
* @property {Object} oldValue The old video dimensions (before the change). This object has two
* properties: height (the old height, in pixels) and width (the old
* width, in pixels).
*
* @see Publisher videoDimensionsChanged
* event
* @see Subscriber videoDimensionsChanged
* event
* @augments Event
*/
Events.VideoDimensionsChangedEvent = function(target, oldValue, newValue) {
Events.Event.call(this, 'videoDimensionsChanged', false);
this.type = 'videoDimensionsChanged';
this.target = target;
this.oldValue = oldValue;
this.newValue = newValue;
};
/**
* Defines event objects for the archiveStarted and archiveStopped
* events. The Session object dispatches these events when an archive recording of the session
* starts and stops.
*
* @property {String} id The archive ID.
* @property {String} name The name of the archive. You can assign an archive a name when you
* create it, using the OpenTok REST API or one
* of the OpenTok server SDKs.
*
* @class ArchiveEvent
* @augments Event
*/
Events.ArchiveEvent = function(type, archive) {
Events.Event.call(this, type, false);
this.type = type;
this.id = archive.id;
this.name = archive.name;
this.status = archive.status;
this.archive = archive;
};
Events.ArchiveUpdatedEvent = function(stream, key, oldValue, newValue) {
Events.Event.call(this, 'updated', false);
this.target = stream;
this.changedProperty = key;
this.oldValue = oldValue;
this.newValue = newValue;
};
/**
* The Session object dispatches a signal event when the client receives a signal from the
* session.
*
* @class SignalEvent
* @property {String} type The type assigned to the signal (if there is one). Use the type to
* filter signals received (by adding an event handler for signal:type1 or signal:type2, etc.)
* @property {String} data The data string sent with the signal (if there is one).
* @property {Connection} from The Connection corresponding to the client that sent with the
* signal.
*
* @see Session.signal()
* @see Session events (signal and signal:type)
* @augments Event
*/
Events.SignalEvent = function(type, data, from) {
Events.Event.call(this, type ? 'signal:' + type : Events.Event.names.SIGNAL, false);
this.data = data;
this.from = from;
};
Events.StreamUpdatedEvent = function(stream, key, oldValue, newValue) {
Events.Event.call(this, 'updated', false);
this.target = stream;
this.changedProperty = key;
this.oldValue = oldValue;
this.newValue = newValue;
};
Events.DestroyedEvent = function(type, target, reason) {
Events.Event.call(this, type, false);
this.target = target;
this.reason = reason;
};
Events.ConnectionStateChangedEvent = function(type, target) {
Events.Event.call(this, type, false);
this.target = target;
};
/**
* Defines the event object for the videoDisabled and videoEnabled
* events dispatched by the Subscriber.
*
* @class VideoEnabledChangedEvent
*
* @property {Boolean} cancelable Whether the event has a default behavior that is cancelable
* (true) or not (false). You can cancel the default behavior by
* calling the preventDefault() method of the event object in the callback
* function. (See preventDefault().)
*
* @property {String} reason The reason the video was disabled or enabled. This can be set to one
* of the following values:
*
* "publishVideo" — The publisher started or stopped publishing video,
* by calling publishVideo(true) or publishVideo(false)."quality" — The OpenTok Media Router starts or stops sending video
* to the subscriber based on stream quality changes. This feature of the OpenTok Media
* Router has a subscriber drop the video stream when connectivity degrades. (The subscriber
* continues to receive the audio stream, if there is one.)
*
* If connectivity improves to support video again, the Subscriber object dispatches
* a videoEnabled event, and the Subscriber resumes receiving video.
*
* By default, the Subscriber displays a video disabled indicator when a
* videoDisabled event with this reason is dispatched and removes the indicator
* when the videoDisabled event with this reason is dispatched. You can control
* the display of this icon by calling the setStyle() method of the Subscriber,
* setting the videoDisabledDisplayMode property(or you can set the style when
* calling the Session.subscribe() method, setting the style property
* of the properties parameter).
*
* This feature is only available in sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed), not in sessions with the media mode set to relayed. *
"subscribeToVideo" — The subscriber started or stopped subscribing to
* video, by calling subscribeToVideo(true) or
* subscribeToVideo(false)."videoDisabled" or
* "videoEnabled".
*
* @see Subscriber videoDisabled event
* @see Subscriber videoEnabled event
* @augments Event
*/
Events.VideoEnabledChangedEvent = function(type, properties) {
Events.Event.call(this, type, false);
this.reason = properties.reason;
};
Events.VideoDisableWarningEvent = function(type/*, properties*/) {
Events.Event.call(this, type, false);
};
/**
* Dispatched periodically by a Subscriber or Publisher object to indicate the audio
* level. This event is dispatched up to 60 times per second, depending on the browser.
*
* @property {Number} audioLevel The audio level, from 0 to 1.0. Adjust this value logarithmically
* for use in adjusting a user interface element, such as a volume meter. Use a moving average
* to smooth the data.
*
* @class AudioLevelUpdatedEvent
* @augments Event
*/
Events.AudioLevelUpdatedEvent = function(audioLevel) {
Events.Event.call(this, Events.Event.names.AUDIO_LEVEL_UPDATED, false);
this.audioLevel = audioLevel;
};
Events.MediaStoppedEvent = function(target) {
Events.Event.call(this, Events.Event.names.MEDIA_STOPPED, true);
this.target = target;
};
/**
* Dispatched by a Subscriber or Publisher object to indicate the video element
* (or object element in Internet Explorer) was created. Add a listener for this event
* when you set the insertDefaultUI option to false in the call to the
* OT.initPublisher() method or the
* Session.subscribe() method. The element
* property of the event object is a reference to the Publisher's video element
* (or the object element in Internet Explorer). Add it to the HTML DOM to display the
* video. (When you set the insertDefaultUI option to false, the
* video element is not inserted into the DOM automatically.)
*
* Add a listener for this event only if you have set the insertDefaultUI option to
* false. If you have not set insertDefaultUI option
* to false, do not move the video element (or the object
* element containing the video in Internet Explorer) in the HTML DOM. Doing so causes the
* Publisher or Subscriber object to be destroyed.
*
* @property {Number} element A reference to the Publisher or Subscriber's video
* element (or in Internet Explorer the object element containing the video).
* Add it to the HTML DOM to display the video.
*
* @class VideoElementCreatedEvent
* @augments Event
*/
Events.VideoElementCreatedEvent = function(element) {
Events.Event.call(this, Events.Event.names.VIDEO_ELEMENT_CREATED, false);
this.element = element;
};
module.exports = Events;
},{"./logging.js":187,"@opentok/ot-helpers":4}],183:[function(require,module,exports){
'use strict';
module.exports = {
JS_EXCEPTION: 2000,
AUTHENTICATION_ERROR: 1004,
INVALID_SESSION_ID: 1005,
CONNECT_FAILED: 1006,
CONNECT_REJECTED: 1007,
CONNECTION_TIMEOUT: 1008,
NOT_CONNECTED: 1010,
INVALID_PARAMETER: 1011,
P2P_CONNECTION_FAILED: 1013,
API_RESPONSE_FAILURE: 1014,
TERMS_OF_SERVICE_FAILURE: 1026,
UNABLE_TO_PUBLISH: 1500,
UNABLE_TO_SUBSCRIBE: 1501,
UNABLE_TO_FORCE_DISCONNECT: 1520,
UNABLE_TO_FORCE_UNPUBLISH: 1530,
PUBLISHER_ICE_WORKFLOW_FAILED: 1553,
SUBSCRIBER_ICE_WORKFLOW_FAILED: 1554,
UNEXPECTED_SERVER_RESPONSE: 2001,
REPORT_ISSUE_ERROR: 2011
};
},{}],184:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
// A Factory method for generating simple state machine classes.
//
// @usage
// var StateMachine = generateSimpleStateMachine('start', ['start', 'middle', 'end', {
// start: ['middle'],
// middle: ['end'],
// end: ['start']
// }]);
//
// var states = new StateMachine();
// state.current; // <-- start
// state.set('middle');
//
module.exports = function generateSimpleStateMachine(initialState, states, transitions) {
var validStates = states.slice();
var validTransitions = OTHelpers.clone(transitions);
var isValidState = function(state) {
return validStates.indexOf(state) !== -1;
};
var isValidTransition = function(fromState, toState) {
return validTransitions[fromState] &&
validTransitions[fromState].indexOf(toState) !== -1;
};
return function(stateChangeFailed) {
var currentState = initialState;
var previousState = null;
this.current = currentState;
function signalChangeFailed(message, newState) {
stateChangeFailed({
message: message,
newState: newState,
currentState: currentState,
previousState: previousState
});
}
// Validates +newState+. If it's invalid it triggers stateChangeFailed and returns false.
function handleInvalidStateChanges(newState) {
if (!isValidState(newState)) {
signalChangeFailed('\'' + newState + '\' is not a valid state', newState);
return false;
}
if (!isValidTransition(currentState, newState)) {
signalChangeFailed('\'' + currentState + '\' cannot transition to \'' +
newState + '\'', newState);
return false;
}
return true;
}
this.set = function(newState) {
if (!handleInvalidStateChanges(newState)) { return; }
previousState = currentState;
this.current = currentState = newState;
};
};
};
},{"@opentok/ot-helpers":4}],185:[function(require,module,exports){
'use strict';
var getMediaDevices = require('../helpers/device_helpers.js').getMediaDevices;
/**
* Enumerates the audio input devices (such as microphones) and video input devices
* (cameras) available to the browser.
*
* The array of devices is passed in as the devices parameter of
* the callback function passed into the method.
*
* @param callback {Function} The callback function invoked when the list of devices
* devices is available. This function takes two parameters:
*
error — This is set to an error object when
* there is an error in calling this method; it is set to null
* when the call succeeds.devices — An array of objects corresponding to
* available microphones and cameras. Each object has three properties: kind,
* deviceId, and label, each of which are strings.
*
* The kind property is set to "audioInput" for audio input
* devices or "videoInput" for video input devices.
*
* The deviceId property is a unique ID for the device. You can pass
* the deviceId in as the audioSource or videoSource
* property of the the options parameter of the
* OT.initPublisher() method.
*
* The label property identifies the device. The label
* property is set to an empty string if the user has not previously granted access to
* a camera and microphone. In HTTP, the user must have granted access to a camera and
* microphone in the current page (for example, in response to a call to
* OT.initPublisher()). In HTTPS, the user must have previously granted access
* to the camera and microphone in the current page or in a page previously loaded from the
* domain.
global.setInterval.
*
* @param {function()} callback
* @param {number} frequency how many times per second we want to execute the callback
* @constructor
*/
module.exports = function IntervalRunner(callback, frequency) {
var _callback = callback;
var _frequency = frequency;
var _intervalId = null;
this.start = function() {
_intervalId = global.setInterval(_callback, 1000 / _frequency);
};
this.stop = function() {
global.clearInterval(_intervalId);
_intervalId = null;
};
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],187:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
var properties = require('../helpers/properties.js');
var logging = {};
module.exports = logging;
OTHelpers.useLogHelpers(logging);
var _debugHeaderLogged = false;
var _setLogLevel = logging.setLogLevel;
// On the first time log level is set to DEBUG (or higher) show version info.
logging.setLogLevel = function(level) {
// Set OTHelpers to the same log level
OTHelpers.setLogLevel(level);
var retVal = _setLogLevel.call(logging, level);
if (logging.shouldLog(logging.DEBUG) && !_debugHeaderLogged) {
logging.debug('OpenTok JavaScript library ' + properties.version);
logging.debug('Release notes: ' + properties.websiteURL +
'/opentok/webrtc/docs/js/release-notes.html');
logging.debug('Known issues: ' + properties.websiteURL +
'/opentok/webrtc/docs/js/release-notes.html#knownIssues');
_debugHeaderLogged = true;
}
logging.debug('OT.setLogLevel(' + retVal + ')');
return retVal;
};
var debugTrue = properties.debug === 'true' || properties.debug === true;
logging.setLogLevel(debugTrue ? logging.DEBUG : logging.ERROR);
// TODO: these methods need to be re-exposed for public use on OT :-(
/**
* Sets the API log level.
*
* Calling OT.setLogLevel() sets the log level for runtime log messages that
* are the OpenTok library generates. The default value for the log level is
* OT.ERROR.
*
* The OpenTok JavaScript library displays log messages in the debugger console (such as * Firebug), if one exists. *
*
* The following example logs the session ID to the console, by calling OT.log().
* The code also logs an error message when it attempts to publish a stream before you have
* connected to the session (by calling Session.connect()).
*
* OT.setLogLevel(OT.LOG);
* session = OT.initSession(sessionId);
* OT.log(sessionId);
* publisher = OT.initPublisher("publishContainer");
* session.publish(publisher);
*
*
* @param {Number} logLevel The degree of logging desired by the developer:
*
* *
OT.NONE API logging is disabled.
* OT.ERROR Logging of errors only.
* OT.WARN Logging of warnings and errors.
* OT.INFO Logging of other useful information, in addition to
* warnings and errors.
* OT.LOG Logging of OT.log() messages, in addition
* to OpenTok info, warning,
* and error messages.
* OT.DEBUG Fine-grained logging of all API actions, as well as
* OT.log() messages.
* OT.LOG or OT.DEBUG,
* by calling OT.setLogLevel(OT.LOG) or OT.setLogLevel(OT.DEBUG).
*
* @param {String} message The string to log.
*
* @name OT.log
* @memberof OT
* @function
* @see OT.setLogLevel()
*/
},{"../helpers/properties.js":154,"@opentok/ot-helpers":4}],188:[function(require,module,exports){
'use strict';
var DelayedEventQueue = function DelayedEventQueue(eventDispatcher) {
var queue = [];
this.enqueue = function enqueue(/* arg1, arg2, ..., argN */) {
queue.push(Array.prototype.slice.call(arguments));
};
this.triggerAll = function triggerAll() {
var event;
// Array.prototype.shift is actually pretty inefficient for longer Arrays,
// this is because after the first element is removed it reshuffles every
// remaining element up one (1). This involves way too many allocations and
// deallocations as the queue size increases.
//
// A more efficient version could be written by keeping an index to the current
// 'first' element in the Array and increasing that by one whenever an element
// is removed. The elements that are infront of the index have been 'deleted'.
// Periodically the front of the Array could be spliced off to reclaim the space.
//
// 1. http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.9
//
//
// TLDR: Array.prototype.shift is O(n), where n is the array length,
// instead of the expected O(1). You can implement your own shift that runs
// in amortised constant time.
//
// @todo benchmark and see if we should actually care about shift's performance
// for our common queue sizes.
//
while ((event = queue.shift())) {
eventDispatcher.trigger.apply(eventDispatcher, event);
}
};
};
module.exports = DelayedEventQueue;
},{}],189:[function(require,module,exports){
'use strict';
// Deserialising a Raptor message mainly means doing a JSON.parse on it.
// We do decorate the final message with a few extra helper properies though.
//
// These include:
// * typeName: A human readable version of the Raptor type. E.g. STREAM instead of 102
// * actionName: A human readable version of the Raptor action. E.g. CREATE instead of 101
// * signature: typeName and actionName combined. This is mainly for debugging. E.g. A type
// of 102 and an action of 101 would result in a signature of "STREAM:CREATE"
//
module.exports = function deserializeMessage(msg) {
if (msg.length === 0) { return {}; }
var message = JSON.parse(msg);
var bits = message.uri.substr(1).split('/');
// Remove the Raptor protocol version
bits.shift();
if (bits[bits.length - 1] === '') { bits.pop(); }
message.params = {};
for (var i = 0, numBits = bits.length; i < numBits - 1; i += 2) {
message.params[bits[i]] = bits[i + 1];
}
// extract the resource name. We special case 'channel' slightly, as
// 'subscriber_channel' or 'stream_channel' is more useful for us
// than 'channel' alone.
if (bits.length % 2 === 0) {
if (bits[bits.length - 2] === 'channel' && bits.length > 6) {
message.resource = bits[bits.length - 4] + '_' + bits[bits.length - 2];
} else {
message.resource = bits[bits.length - 2];
}
} else if (bits[bits.length - 1] === 'channel' && bits.length > 5) {
message.resource = bits[bits.length - 3] + '_' + bits[bits.length - 1];
} else {
message.resource = bits[bits.length - 1];
}
message.signature = message.resource + '#' + message.method;
return message;
};
},{}],190:[function(require,module,exports){
'use strict';
var OTError = require('../../ot_error.js');
var OTHelpers = require('@opentok/ot-helpers');
var logging = require('../../logging.js');
var RumorMessageTypes = require('../rumor/rumor_message_types.js');
var unboxFromRumorMessage = require('./unbox_from_rumor_message.js');
var Dispatcher = function() {
OTHelpers.eventing(this, true);
this.callbacks = {};
};
module.exports = Dispatcher;
Dispatcher.prototype.registerCallback = function(transactionId, completion) {
this.callbacks[transactionId] = completion;
};
Dispatcher.prototype.triggerCallback = function(transactionId) {
/*, arg1, arg2, argN-1, argN*/
if (!transactionId) { return; }
var completion = this.callbacks[transactionId];
if (completion && OTHelpers.isFunction(completion)) {
var args = Array.prototype.slice.call(arguments);
args.shift();
completion.apply(null, args);
}
delete this.callbacks[transactionId];
};
Dispatcher.prototype.onClose = function(reason) {
this.emit('close', reason);
};
Dispatcher.prototype.onReconnected = function() {
this.emit('reconnected');
};
Dispatcher.prototype.onReconnecting = function() {
this.emit('reconnecting');
};
Dispatcher.prototype.dispatch = function(rumorMessage) {
// The special casing of STATUS messages is ugly. Need to think about
// how to better integrate this.
if (rumorMessage.type === RumorMessageTypes.STATUS) {
logging.debug('OT.Raptor.dispatch: STATUS');
logging.debug(rumorMessage);
var error;
if (rumorMessage.isError) {
error = new OTError(rumorMessage.status);
}
this.triggerCallback(rumorMessage.transactionId, error, rumorMessage);
return;
}
var message = unboxFromRumorMessage(rumorMessage);
logging.debug('OT.Raptor.dispatch ' + message.signature);
logging.debug(JSON.stringify(message, null, 2));
switch (message.resource) {
case 'session':
this.dispatchSession(message);
break;
case 'connection':
this.dispatchConnection(message);
break;
case 'stream':
this.dispatchStream(message);
break;
case 'stream_channel':
this.dispatchStreamChannel(message);
break;
case 'subscriber':
this.dispatchSubscriber(message);
break;
case 'subscriber_channel':
this.dispatchSubscriberChannel(message);
break;
case 'signal':
this.dispatchSignal(message);
break;
case 'archive':
this.dispatchArchive(message);
break;
default:
logging.warn(
'OT.Raptor.dispatch: Type ' + message.resource + ' is not currently implemented'
);
}
};
Dispatcher.prototype.dispatchSession = function(message) {
switch (message.method) {
case 'read':
this.emit('session#read', message.content, message.transactionId);
break;
default:
logging.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
}
};
Dispatcher.prototype.dispatchConnection = function(message) {
switch (message.method) {
case 'created':
this.emit('connection#created', message.content);
break;
case 'deleted':
this.emit('connection#deleted', message.params.connection, message.reason);
break;
default:
logging.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
}
};
Dispatcher.prototype.dispatchStream = function(message) {
switch (message.method) {
case 'created':
this.emit('stream#created', message.content, message.transactionId);
break;
case 'deleted':
this.emit('stream#deleted', message.params.stream,
message.reason);
break;
case 'updated':
this.emit('stream#updated', message.params.stream,
message.content);
break;
// The JSEP process
case 'generateoffer':
case 'answer':
case 'pranswer':
case 'offer':
case 'candidate':
this.dispatchJsep(message.method, message);
break;
default:
logging.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
}
};
Dispatcher.prototype.dispatchStreamChannel = function(message) {
switch (message.method) {
case 'updated':
this.emit('streamChannel#updated', message.params.stream,
message.params.channel, message.content);
break;
default:
logging.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
}
};
// Dispatch JSEP messages
//
// generateoffer:
// Request to generate a offer for another Peer (or Prism). This kicks
// off the JSEP process.
//
// answer:
// generate a response to another peers offer, this contains our constraints
// and requirements.
//
// pranswer:
// a provisional answer, i.e. not the final one.
//
// candidate
//
//
Dispatcher.prototype.dispatchJsep = function(method, message) {
this.emit('jsep#' + method, message.params.stream, message.fromAddress, message);
};
Dispatcher.prototype.dispatchSubscriberChannel = function(message) {
switch (message.method) {
case 'updated':
this.emit('subscriberChannel#updated', message.params.stream,
message.params.channel, message.content);
break;
case 'update': // subscriberId, streamId, content
this.emit('subscriberChannel#update', message.params.subscriber,
message.params.stream, message.content);
break;
default:
logging.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
}
};
Dispatcher.prototype.dispatchSubscriber = function(message) {
switch (message.method) {
case 'created':
this.emit('subscriber#created', message.params.stream, message.fromAddress,
message.content.id);
break;
case 'deleted':
this.dispatchJsep('unsubscribe', message);
this.emit('subscriber#deleted', message.params.stream,
message.fromAddress);
break;
// The JSEP process
case 'generateoffer':
case 'answer':
case 'pranswer':
case 'offer':
case 'candidate':
this.dispatchJsep(message.method, message);
break;
default:
logging.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
}
};
Dispatcher.prototype.dispatchSignal = function(message) {
if (message.method !== 'signal') {
logging.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
return;
}
this.emit('signal', message.fromAddress, message.content.type,
message.content.data);
};
Dispatcher.prototype.dispatchArchive = function(message) {
switch (message.method) {
case 'created':
this.emit('archive#created', message.content);
break;
case 'updated':
this.emit('archive#updated', message.params.archive, message.content);
break;
default:
}
};
},{"../../logging.js":187,"../../ot_error.js":208,"../rumor/rumor_message_types.js":205,"./unbox_from_rumor_message.js":199,"@opentok/ot-helpers":4}],191:[function(require,module,exports){
'use strict';
var RaptorConstants = require('./raptor_constants.js');
var Raptor = {};
Raptor.Actions = RaptorConstants.Actions;
Raptor.Types = RaptorConstants.Types;
Raptor.Dispatcher = require('./dispatcher.js');
Raptor.serializeMessage = require('./serialize_message.js');
Raptor.deserializeMessage = require('./deserialize_message.js');
Raptor.unboxFromRumorMessage = require('./unbox_from_rumor_message.js');
Raptor.parseIceServers = require('./parse_ice_servers.js');
Raptor.Message = require('./message.js');
Raptor.Socket = require('./raptor_socket.js');
module.exports = Raptor;
},{"./deserialize_message.js":189,"./dispatcher.js":190,"./message.js":192,"./parse_ice_servers.js":193,"./raptor_constants.js":194,"./raptor_socket.js":195,"./serialize_message.js":196,"./unbox_from_rumor_message.js":199}],192:[function(require,module,exports){
'use strict';
var uuid = require('uuid');
var OTHelpers = require('@opentok/ot-helpers');
var serializeMessage = require('./serialize_message.js');
var supportedCryptoScheme = require('../../../helpers/supported_crypto_scheme.js');
var properties = require('../../../helpers/properties.js');
var Message = {};
module.exports = Message;
Message.offer = function(uri, offerSdp) {
return serializeMessage({
method: 'offer',
uri: uri,
content: {
sdp: offerSdp
}
});
};
Message.connections = {};
Message.connections.create = function(apiKey, sessionId, connectionId, capabilities) {
return serializeMessage({
method: 'create',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/connection/' + connectionId,
content: {
userAgent: OTHelpers.env.userAgent,
clientVersion: 'js-' + properties.version.replace('v', ''),
capabilities: capabilities || []
}
});
};
Message.connections.destroy = function(apiKey, sessionId, connectionId) {
return serializeMessage({
method: 'delete',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/connection/' + connectionId,
content: {}
});
};
Message.sessions = {};
Message.sessions.get = function(apiKey, sessionId) {
return serializeMessage({
method: 'read',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId,
content: {}
});
};
Message.streams = {};
Message.streams.get = function(apiKey, sessionId, streamId) {
return serializeMessage({
method: 'read',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId,
content: {}
});
};
Message.streams.channelFromOTChannel = function(channel) {
var raptorChannel = {
id: channel.id,
type: channel.type,
active: channel.active
};
if (channel.type === 'video') {
raptorChannel.width = channel.width;
raptorChannel.height = channel.height;
raptorChannel.orientation = channel.orientation;
raptorChannel.frameRate = channel.frameRate;
if (channel.source !== 'default') {
raptorChannel.source = channel.source;
}
raptorChannel.fitMode = channel.fitMode;
}
return raptorChannel;
};
Message.streams.create = function(apiKey, sessionId, streamId, name,
audioFallbackEnabled, channels, minBitrate, maxBitrate) {
var messageContent = {
id: streamId,
name: name,
audioFallbackEnabled: audioFallbackEnabled,
channel: channels.map(function(channel) {
return Message.streams.channelFromOTChannel(channel);
})
};
if (minBitrate) { messageContent.minBitrate = minBitrate; }
if (maxBitrate) { messageContent.maxBitrate = maxBitrate; }
return serializeMessage({
method: 'create',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId,
content: messageContent
});
};
Message.streams.destroy = function(apiKey, sessionId, streamId) {
return serializeMessage({
method: 'delete',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId,
content: {}
});
};
Message.streams.answer = function(apiKey, sessionId, streamId, answerSdp) {
return serializeMessage({
method: 'answer',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId,
content: {
sdp: answerSdp
}
});
};
Message.streams.candidate = function(apiKey, sessionId, streamId, candidate) {
return serializeMessage({
method: 'candidate',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId,
content: candidate
});
};
Message.streamChannels = {};
Message.streamChannels.update =
function(apiKey, sessionId, streamId, channelId, attributes) {
return serializeMessage({
method: 'update',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' +
streamId + '/channel/' + channelId,
content: attributes
});
};
Message.subscribers = {};
Message.subscribers.uri = function(apiKey, sessionId, streamId, subscriberId) {
return '/v2/partner/' + apiKey + '/session/' + sessionId +
'/stream/' + streamId + '/subscriber/' + subscriberId;
};
Message.subscribers.create =
function(apiKey, sessionId, streamId, subscriberId, connectionId, channelsToSubscribeTo) {
var content = {
id: subscriberId,
connection: connectionId,
keyManagementMethod: supportedCryptoScheme(),
bundleSupport: OTHelpers.hasCapabilities('bundle'),
rtcpMuxSupport: OTHelpers.hasCapabilities('RTCPMux')
};
if (channelsToSubscribeTo) { content.channel = channelsToSubscribeTo; }
return serializeMessage({
method: 'create',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
'/stream/' + streamId + '/subscriber/' + subscriberId,
content: content
});
};
Message.subscribers.destroy =
function(apiKey, sessionId, streamId, subscriberId) {
return serializeMessage({
method: 'delete',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
'/stream/' + streamId + '/subscriber/' + subscriberId,
content: {}
});
};
Message.subscribers.update =
function(apiKey, sessionId, streamId, subscriberId, attributes) {
return serializeMessage({
method: 'update',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
'/stream/' + streamId + '/subscriber/' + subscriberId,
content: attributes
});
};
Message.subscribers.candidate =
function(apiKey, sessionId, streamId, subscriberId, candidate) {
return serializeMessage({
method: 'candidate',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
'/stream/' + streamId + '/subscriber/' + subscriberId,
content: candidate
});
};
Message.subscribers.answer =
function(apiKey, sessionId, streamId, subscriberId, answerSdp) {
return serializeMessage({
method: 'answer',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
'/stream/' + streamId + '/subscriber/' + subscriberId,
content: {
sdp: answerSdp
}
});
};
Message.subscriberChannels = {};
Message.subscriberChannels.update =
function(apiKey, sessionId, streamId, subscriberId, channelId, attributes) {
return serializeMessage({
method: 'update',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
'/stream/' + streamId + '/subscriber/' + subscriberId + '/channel/' + channelId,
content: attributes
});
};
Message.signals = {};
Message.signals.create =
function(apiKey, sessionId, toAddress, type, data) {
var content = {};
if (type !== void 0) { content.type = type; }
if (data !== void 0) { content.data = data; }
return serializeMessage({
method: 'signal',
uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
(toAddress !== void 0 ? '/connection/' + toAddress : '') + '/signal/' + uuid(),
content: content
});
};
},{"../../../helpers/properties.js":154,"../../../helpers/supported_crypto_scheme.js":155,"./serialize_message.js":196,"@opentok/ot-helpers":4,"uuid":137}],193:[function(require,module,exports){
'use strict';
module.exports = function parseIceServers(message) {
var iceServers;
try {
iceServers = JSON.parse(message.data).content.iceServers;
} catch (e) {
return [];
}
return iceServers.map(function(iceServer) {
return {
url: iceServer.url, // soon to be deprecated
urls: iceServer.url,
username: iceServer.username,
credential: iceServer.credential
};
});
};
},{}],194:[function(require,module,exports){
'use strict';
// Rumor Messaging for JS
//
// https://tbwiki.tokbox.com/index.php/Raptor_Messages_(Sent_as_a_RumorMessage_payload_in_JSON)
//
// @todo Raptor {
// Look at disconnection cleanup: i.e. subscriber + publisher cleanup
// Add error codes for all the error cases
// Write unit tests for SessionInfo
// Write unit tests for Session
// Make use of the new DestroyedEvent
// Capabilities must be part of the Raptor namespace
// Add Dependability commands
// Think about noConflict, or whether we should just use the OT namespace
// Think about how to expose sessionObjects.publishers, sessionObjects.subscribers, and
// sessionObjects.sessions if messaging was being included as a component
// Another solution to the problem of having publishers/subscribers/etc would be to make
// Raptor Socket a separate component from Dispatch (dispatch being more business logic)
// Look at the coupling of sessionObjects.sessions to RaptorSocket
// }
//
// @todo Raptor Docs {
// Document payload formats for incoming messages (what are the payloads for
// STREAM CREATED/MODIFIED for example)
// Document how keepalives work
// Document all the Raptor actions and types
// Document the session connect flow (including error cases)
// }
module.exports = {
Actions: {
//General
CONNECT: 100,
CREATE: 101,
UPDATE: 102,
DELETE: 103,
STATE: 104,
//Moderation
FORCE_DISCONNECT: 105,
FORCE_UNPUBLISH: 106,
SIGNAL: 107,
//Archives
CREATE_ARCHIVE: 108,
CLOSE_ARCHIVE: 109,
START_RECORDING_SESSION: 110,
STOP_RECORDING_SESSION: 111,
START_RECORDING_STREAM: 112,
STOP_RECORDING_STREAM: 113,
LOAD_ARCHIVE: 114,
START_PLAYBACK: 115,
STOP_PLAYBACK: 116,
//AppState
APPSTATE_PUT: 117,
APPSTATE_DELETE: 118,
// JSEP
OFFER: 119,
ANSWER: 120,
PRANSWER: 121,
CANDIDATE: 122,
SUBSCRIBE: 123,
UNSUBSCRIBE: 124,
QUERY: 125,
SDP_ANSWER: 126,
//KeepAlive
PONG: 127,
REGISTER: 128, //Used for registering streams.
QUALITY_CHANGED: 129
},
Types: {
//RPC
RPC_REQUEST: 100,
RPC_RESPONSE: 101,
//EVENT
STREAM: 102,
ARCHIVE: 103,
CONNECTION: 104,
APPSTATE: 105,
CONNECTIONCOUNT: 106,
MODERATION: 107,
SIGNAL: 108,
SUBSCRIBER: 110,
//JSEP Protocol
JSEP: 109
}
};
},{}],195:[function(require,module,exports){
'use strict';
var assign = require('lodash.assign');
var uuid = require('uuid');
var analytics = require('../../analytics.js');
var ExceptionCodes = require('../../exception_codes.js');
var Dispatcher = require('./dispatcher.js');
var logging = require('../../logging.js');
var Message = require('./message.js');
var OTError = require('../../ot_error.js');
var OTHelpers = require('@opentok/ot-helpers');
var Signal = require('./signal.js');
var RumorSocket = require('../rumor/rumor_socket.js');
function SignalError(code, message) {
this.code = code;
this.message = message;
// Undocumented. Left in for backwards compatibility:
this.reason = message;
}
// The Dispatcher bit is purely to make testing simpler, it defaults to a new Dispatcher so in
// normal operation you would omit it.
var RaptorSocket = function RaptorSocket(
connectionId,
widgetId,
messagingSocketUrl,
symphonyUrl,
dispatcher
) {
var _apiKey, _sessionId, _token, _rumor, _completion, _p2p, _messagingServer;
var _states = ['disconnected', 'connecting', 'connected', 'error', 'disconnecting'];
var _dispatcher = dispatcher || new Dispatcher();
//// Private API
var setState = OTHelpers.statable(this, _states, 'disconnected');
var logAnalyticsEvent = function(opt) {
if (!opt.action || !opt.variation) {
logging.warn('Expected action and variation');
}
analytics.logEvent(assign(
{
sessionId: _sessionId,
partnerId: _apiKey,
p2p: _p2p,
messagingServer: _messagingServer,
connectionId: connectionId
},
opt
));
};
var onConnectComplete = function onConnectComplete(error) {
if (error) {
setState('error');
} else {
setState('connected');
}
_completion.apply(null, arguments);
};
var onClose = function onClose(err) {
var reason = 'clientDisconnected';
if (!this.is('disconnecting') && _rumor.is('error')) {
reason = 'networkDisconnected';
}
if (err && err.code === 4001) {
reason = 'networkTimedout';
}
setState('disconnected');
_dispatcher.onClose(reason);
}.bind(this);
var onError = function onError(err) {
logging.error('OT.Raptor.Socket error:', err);
};
// @todo what does having an error mean? Are they always fatal? Are we disconnected now?
var onReconnecting = function onReconnecting() {
_dispatcher.onReconnecting();
};
var onReconnected = function onReconnected() {
logAnalyticsEvent({
action: 'Reconnect',
variation: 'Success',
retries: _rumor.reconnectRetriesCount(),
messageQueueSize: _rumor.messageQueueSize(),
socketId: _rumor.socketID()
});
_dispatcher.onReconnected();
};
var onReconnectAttempt = function onReconnectAttempt() {
logAnalyticsEvent({
action: 'Reconnect',
variation: 'Attempt',
retries: _rumor.reconnectRetriesCount(),
messageQueueSize: _rumor.messageQueueSize(),
socketId: _rumor.socketID()
});
};
var convertRumorConnectError = function convertRumorConnectError(error) {
var errorCode, errorMessage;
var knownErrorCodes = [400, 403, 409];
if (error.code === ExceptionCodes.CONNECT_FAILED) {
errorCode = error.code;
errorMessage = OTError.getTitleByCode(error.code);
} else if (error.code && knownErrorCodes.indexOf(error.code) > -1) {
errorCode = ExceptionCodes.CONNECT_FAILED;
errorMessage = 'Received error response to connection create message.';
} else {
errorCode = ExceptionCodes.UNEXPECTED_SERVER_RESPONSE;
errorMessage = 'Unexpected server response. Try this operation again later.';
}
return {
errorCode: errorCode,
errorMessage: errorMessage
};
};
var onReconnectFailure = function onReconnectFailure(error) {
var converted = convertRumorConnectError(error);
logAnalyticsEvent({
action: 'Reconnect',
variation: 'Failure',
failureReason: 'ConnectToSession',
failureCode: converted.errorCode,
failureMessage: converted.errorMessage,
messageQueueSize: _rumor.messageQueueSize(),
socketId: _rumor.socketID()
});
};
//// Public API
this.connect = function(token, sessionInfo, completion) {
if (!this.is('disconnected', 'error')) {
logging.warn('Cannot connect the Raptor Socket as it is currently connected. You should ' +
'disconnect first.');
return;
}
setState('connecting');
_apiKey = sessionInfo.partnerId;
_sessionId = sessionInfo.sessionId;
_p2p = sessionInfo.p2pEnabled;
_messagingServer = sessionInfo.messagingServer;
_token = token;
_completion = completion;
var rumorChannel = '/v2/partner/' + _apiKey + '/session/' + _sessionId;
_rumor = new RaptorSocket.RumorSocket({
messagingURL: messagingSocketUrl,
notifyDisconnectAddress: symphonyUrl,
connectionId: connectionId,
enableReconnection: sessionInfo.reconnection
});
_rumor.onClose(onClose);
_rumor.onError(onError);
_rumor.onReconnecting(onReconnecting);
_rumor.onReconnectAttempt(onReconnectAttempt);
_rumor.onReconnectFailure(onReconnectFailure);
_rumor.onReconnected(onReconnected);
_rumor.onMessage(_dispatcher.dispatch.bind(_dispatcher));
_rumor.connect(function(error) {
if (error) {
onConnectComplete({
reason: 'WebSocketConnection',
code: error.code,
message: error.message
});
return;
}
logging.debug('OT.Raptor.Socket connected. Subscribing to ' +
rumorChannel + ' on ' + messagingSocketUrl);
_rumor.subscribe([rumorChannel]);
var capabilities = [];
if (OTHelpers.hasCapabilities('iceRestarts') && sessionInfo.renegotiation) {
capabilities.push('renegotiation');
}
//connect to session
var connectMessage = Message.connections.create(
_apiKey,
_sessionId,
_rumor.id(),
capabilities
);
this.publish(connectMessage, { 'X-TB-TOKEN-AUTH': _token }, true, function(error, reply) {
if (error) {
var converted = convertRumorConnectError(error);
logAnalyticsEvent({
action: 'Connect',
variation: 'Failure',
failureReason: 'ConnectToSession',
failureCode: converted.errorCode,
failureMessage: converted.errorMessage,
socketId: _rumor.socketID()
});
onConnectComplete({
reason: 'ConnectToSession',
code: converted.errorCode,
message: converted.errorMessage,
socketId: _rumor.socketID()
});
return;
}
var onSessionState = function onSessionState(error, sessionState) {
if (error) {
var errorCode, errorMessage;
var knownErrorCodes = [400, 403, 409];
if (knownErrorCodes.indexOf(error.code) > -1) {
errorCode = ExceptionCodes.CONNECT_FAILED;
errorMessage = 'Received error response to session read';
} else {
errorCode = ExceptionCodes.UNEXPECTED_SERVER_RESPONSE;
errorMessage = 'Unexpected server response. Try this operation again later.';
}
logAnalyticsEvent({
action: 'Connect',
variation: 'Failure',
failureReason: 'GetSessionState',
failureCode: errorCode,
failureMessage: errorMessage,
socketId: _rumor.socketID()
});
onConnectComplete({
reason: 'GetSessionState',
code: errorCode,
message: errorMessage,
socketId: _rumor.socketID()
});
} else {
onConnectComplete(undefined, sessionState);
}
};
if (reply.data) {
// OPENTOK-27994: Unfortunately, we need to send a fake session#read into the dispatcher
// in order to get the right side effects to happen to the Session. Ideally, we could
// transform the data here and send it into the completion handler for Session to decide
// for itself what to do with it. The problem is that SessionDispatcher contains both
// transformation code that belongs here and Session manipulation that belongs in Session,
// and refactoring that structure isn't appropriate right now.
var transactionId = uuid();
_dispatcher.registerCallback(transactionId, onSessionState);
_dispatcher.emit('session#read', JSON.parse(reply.data), transactionId);
} else {
// Older implementations do not send session#read data in the connect reply, so we have
// to get it the old way (OPENTOK-27775).
this.publish(Message.sessions.get(_apiKey, _sessionId), {}, true, onSessionState);
}
}.bind(this));
}.bind(this));
};
this.disconnect = function(drainSocketBuffer) {
if (this.is('disconnected')) {
return;
}
setState('disconnecting');
_rumor.disconnect(drainSocketBuffer);
};
// Publishes +message+ to the Symphony app server.
//
// The completion handler is optional, as is the headers
// dict, but if you provide the completion handler it must
// be the last argument.
//
this.publish = function(message, headers, retryAfterReconnect, completion) {
if (_rumor.isNot('connected', 'reconnecting')) {
logging.error('OT.Raptor.Socket: cannot publish until the socket is connected.' + message);
return undefined;
}
var transactionId = uuid();
if (completion) {
_dispatcher.registerCallback(transactionId, completion);
}
logging.debug('OT.Raptor.Socket Publish (ID:' + transactionId + ')');
logging.debug(message);
_rumor.publish([symphonyUrl], message, OTHelpers.extend({}, headers, {
'Content-Type': 'application/x-raptor+v2',
'TRANSACTION-ID': transactionId,
'X-TB-FROM-ADDRESS': _rumor.id()
}), retryAfterReconnect);
return transactionId;
};
// Register a new stream against _sessionId
this.streamCreate = function(name, streamId, audioFallbackEnabled, channels, minBitrate,
maxBitrate, completion) {
var message = Message.streams.create(_apiKey,
_sessionId,
streamId,
name,
audioFallbackEnabled,
channels,
minBitrate,
maxBitrate);
this.publish(message, {}, true, function(error, message) {
completion(error, streamId, message);
});
};
this.streamDestroy = function(streamId) {
this.publish(Message.streams.destroy(_apiKey, _sessionId, streamId), {}, true);
};
this.streamChannelUpdate = function(streamId, channelId, attributes) {
this.publish(Message.streamChannels.update(_apiKey, _sessionId,
streamId, channelId, attributes), {}, true);
};
this.subscriberCreate = function(streamId, subscriberId, channelsToSubscribeTo, completion) {
this.publish(Message.subscribers.create(_apiKey, _sessionId,
streamId, subscriberId, _rumor.id(), channelsToSubscribeTo), {}, true, completion);
};
this.subscriberDestroy = function(streamId, subscriberId) {
this.publish(Message.subscribers.destroy(_apiKey, _sessionId,
streamId, subscriberId), {}, true);
};
this.subscriberUpdate = function(streamId, subscriberId, attributes) {
this.publish(Message.subscribers.update(_apiKey, _sessionId,
streamId, subscriberId, attributes), {}, true);
};
this.subscriberChannelUpdate = function(streamId, subscriberId, channelId, attributes) {
this.publish(Message.subscriberChannels.update(_apiKey, _sessionId,
streamId, subscriberId, channelId, attributes), {}, true);
};
this.forceDisconnect = function(connectionIdToDisconnect, completion) {
this.publish(Message.connections.destroy(_apiKey, _sessionId,
connectionIdToDisconnect), {}, true, completion);
};
this.forceUnpublish = function(streamIdToUnpublish, completion) {
this.publish(Message.streams.destroy(_apiKey, _sessionId,
streamIdToUnpublish), {}, true, completion);
};
this.jsepCandidate = function(streamId, candidate) {
this.publish(
Message.streams.candidate(_apiKey, _sessionId, streamId, candidate), {}, true
);
};
this.jsepCandidateP2p = function(streamId, subscriberId, candidate) {
this.publish(
Message.subscribers.candidate(_apiKey, _sessionId, streamId,
subscriberId, candidate), {}, true
);
};
this.jsepOffer = function(uri, offerSdp) {
this.publish(Message.offer(uri, offerSdp), {}, true);
};
this.jsepAnswer = function(streamId, answerSdp) {
this.publish(Message.streams.answer(_apiKey, _sessionId, streamId, answerSdp), {}, true);
};
this.jsepAnswerP2p = function(streamId, subscriberId, answerSdp) {
this.publish(Message.subscribers.answer(_apiKey, _sessionId, streamId,
subscriberId, answerSdp), {}, true);
};
this.signal = function(options, completion, logEventFn) {
var signal = new Signal(_sessionId, _rumor.id(), options || {});
if (!signal.valid) {
if (completion && OTHelpers.isFunction(completion)) {
completion(new SignalError(signal.error.code, signal.error.reason), signal.toHash());
}
return;
}
this.publish(signal.toRaptorMessage(), {}, signal.retryAfterReconnect, function(err) {
var error, errorCode, errorMessage;
var expectedErrorCodes = [400, 403, 404, 413];
if (err) {
if (err.code && expectedErrorCodes.indexOf(err.code) > -1) {
errorCode = err.code;
errorMessage = err.message;
} else {
errorCode = ExceptionCodes.UNEXPECTED_SERVER_RESPONSE;
errorMessage = 'Unexpected server response. Try this operation again later.';
}
error = new SignalError(errorCode, errorMessage);
} else {
var typeStr = signal.data ? typeof (signal.data) : null;
logEventFn('signal', 'send', { type: typeStr });
}
if (completion && OTHelpers.isFunction(completion)) { completion(error, signal.toHash()); }
});
};
this.id = function() {
return _rumor && _rumor.id();
};
};
RaptorSocket.RumorSocket = RumorSocket;
module.exports = RaptorSocket;
},{"../../analytics.js":166,"../../exception_codes.js":183,"../../logging.js":187,"../../ot_error.js":208,"../rumor/rumor_socket.js":206,"./dispatcher.js":190,"./message.js":192,"./signal.js":198,"@opentok/ot-helpers":4,"lodash.assign":78,"uuid":137}],196:[function(require,module,exports){
'use strict';
module.exports = function serializeMessage(message) {
return JSON.stringify(message);
};
},{}],197:[function(require,module,exports){
'use strict';
var Archive = require('../../archive.js');
var Connection = require('../../connection.js');
var DelayedEventQueue = require('./delayed_event_queue.js');
var Dispatcher = require('./dispatcher.js');
var logging = require('../../logging.js');
var OTHelpers = require('@opentok/ot-helpers');
var sessionObjects = require('../../session/objects.js');
var Stream = require('../../stream.js');
var StreamChannel = require('../../stream_channel.js');
function parseStream(dict, session) {
var channel = dict.channel.map(function(channel) {
return new StreamChannel(channel);
});
var connectionId = dict.connectionId ? dict.connectionId : dict.connection.id;
return new Stream(dict.id,
dict.name,
dict.creationTime,
session.connections.get(connectionId),
session,
channel);
}
function parseAndAddStreamToSession(dict, session) {
if (session.streams.has(dict.id)) { return undefined; }
var stream = parseStream(dict, session);
session.streams.add(stream);
return stream;
}
function parseArchive(dict) {
return new Archive(dict.id,
dict.name,
dict.status);
}
function parseAndAddArchiveToSession(dict, session) {
if (session.archives.has(dict.id)) { return undefined; }
var archive = parseArchive(dict);
session.archives.add(archive);
return archive;
}
var DelayedSessionEvents = function(dispatcher) {
var eventQueues = {};
this.enqueue = function enqueue(/* key, arg1, arg2, ..., argN */) {
var key = arguments[0];
var eventArgs = Array.prototype.slice.call(arguments, 1);
if (!eventQueues[key]) {
eventQueues[key] = new DelayedEventQueue(dispatcher);
}
eventQueues[key].enqueue.apply(eventQueues[key], eventArgs);
};
this.triggerConnectionCreated = function triggerConnectionCreated(connection) {
if (eventQueues['connectionCreated' + connection.id]) {
eventQueues['connectionCreated' + connection.id].triggerAll();
}
};
this.triggerSessionConnected = function triggerSessionConnected(connections) {
if (eventQueues.sessionConnected) {
eventQueues.sessionConnected.triggerAll();
}
connections.forEach(function(connection) {
this.triggerConnectionCreated(connection);
}, this);
};
};
var unconnectedStreams = {};
module.exports = function SessionDispatcher(session) {
var dispatcher = new Dispatcher();
var sessionStateReceived = false;
var delayedSessionEvents = new DelayedSessionEvents(dispatcher);
dispatcher.on('reconnecting', function() {
session._.reconnecting();
});
dispatcher.on('reconnected', function() {
session._.reconnected();
});
dispatcher.on('close', function(reason) {
var connection = session.connection;
if (!connection) {
return;
}
if (connection.destroyedReason()) {
logging.debug('OT.Raptor.Socket: Socket was closed but the connection had already ' +
'been destroyed. Reason: ' + connection.destroyedReason());
return;
}
connection.destroy(reason);
});
// This method adds connections to the session both on a connection#created and
// on a session#read. In the case of session#read sessionRead is set to true and
// we include our own connection.
var addConnection = function(connection, sessionRead) {
connection = Connection.fromHash(connection);
if (sessionRead || session.connection && connection.id !== session.connection.id) {
session.connections.add(connection);
delayedSessionEvents.triggerConnectionCreated(connection);
}
Object.keys(unconnectedStreams).forEach(function(streamId) {
var stream = unconnectedStreams[streamId];
if (stream && connection.id === stream.connection.id) {
// dispatch streamCreated event now that the connectionCreated has been dispatched
parseAndAddStreamToSession(stream, session);
delete unconnectedStreams[stream.id];
var payload = {
debug: sessionRead ? 'connection came in session#read' :
'connection came in connection#created',
streamId: stream.id,
connectionId: connection.id
};
session.logEvent('streamCreated', 'warning', payload);
}
});
return connection;
};
dispatcher.on('session#read', function(content, transactionId) {
var connection;
var state = {};
state.streams = [];
state.connections = [];
state.archives = [];
content.connection.forEach(function(connectionParams) {
connection = addConnection(connectionParams, true);
state.connections.push(connection);
});
content.stream.forEach(function(streamParams) {
state.streams.push(parseAndAddStreamToSession(streamParams, session));
});
(content.archive || content.archives).forEach(function(archiveParams) {
state.archives.push(parseAndAddArchiveToSession(archiveParams, session));
});
session._.subscriberMap = {};
dispatcher.triggerCallback(transactionId, null, state);
sessionStateReceived = true;
delayedSessionEvents.triggerSessionConnected(session.connections);
});
dispatcher.on('connection#created', function(connection) {
addConnection(connection);
});
dispatcher.on('connection#deleted', function(connection, reason) {
connection = session.connections.get(connection);
connection.destroy(reason);
});
dispatcher.on('stream#created', function(stream, transactionId) {
var connectionId = stream.connectionId ? stream.connectionId : stream.connection.id;
if (session.connections.has(connectionId)) {
stream = parseAndAddStreamToSession(stream, session);
} else {
unconnectedStreams[stream.id] = stream;
var payload = {
debug: 'eventOrderError -- streamCreated event before connectionCreated',
streamId: stream.id
};
session.logEvent('streamCreated', 'warning', payload);
}
if (stream.publisher) {
stream.publisher.setStream(stream);
}
dispatcher.triggerCallback(transactionId, null, stream);
});
dispatcher.on('stream#deleted', function(streamId, reason) {
var stream = session.streams.get(streamId);
if (!stream) {
logging.error('OT.Raptor.dispatch: A stream does not exist with the id of ' +
streamId + ', for stream#deleted message!');
// @todo error
return;
}
stream.destroy(reason);
});
dispatcher.on('stream#updated', function(streamId, content) {
var stream = session.streams.get(streamId);
if (!stream) {
logging.error('OT.Raptor.dispatch: A stream does not exist with the id of ' +
streamId + ', for stream#updated message!');
// @todo error
return;
}
stream._.update(content);
});
dispatcher.on('streamChannel#updated', function(streamId, channelId, content) {
var stream;
if (!(streamId && (stream = session.streams.get(streamId)))) {
logging.error('OT.Raptor.dispatch: Unable to determine streamId, or the stream does not ' +
'exist, for streamChannel message!');
// @todo error
return;
}
stream._.updateChannel(channelId, content);
});
// Dispatch JSEP messages
//
// generateoffer:
// Request to generate a offer for another Peer (or Prism). This kicks
// off the JSEP process.
//
// answer:
// generate a response to another peers offer, this contains our constraints
// and requirements.
//
// pranswer:
// a provisional answer, i.e. not the final one.
//
// candidate
//
//
// This function takes an array of collections and a matching function
// If it finds a match in the first collection it returns that item otherwise
// it returns a match in the second collection. The return value is an array of
// 1 item or an empty array if there is no match.
var findItems = function(fromCollections, whereClause) {
return fromCollections.reduce(function(returnVal, collection) {
if (returnVal.length === 0) {
var item = collection.find(whereClause);
return item ? [item] : [];
}
return returnVal;
}, []);
};
var jsepHandler = function(method, streamId, fromAddress, message) {
var whereClause = { streamId: streamId };
var subscribers = sessionObjects.subscribers;
var publishers = sessionObjects.publishers;
var fromConnection, actors;
// Determine which subscriber/publisher objects should receive this message.
switch (method) {
case 'offer':
actors = findItems([subscribers, publishers], whereClause);
break;
case 'answer':
case 'pranswer':
actors = findItems([publishers, subscribers], whereClause);
break;
case 'generateoffer':
case 'unsubscribe':
actors = findItems([publishers], whereClause);
break;
case 'candidate':
actors = subscribers.where(whereClause).concat(publishers.where(whereClause));
break;
default:
logging.warn('OT.Raptor.dispatch: jsep#' + method +
' is not currently implemented');
return;
}
if (actors.length === 0) { return; }
fromConnection = session.connections.get(fromAddress);
if (!fromConnection && fromAddress.match(/^symphony\./)) {
fromConnection = Connection.fromHash({
id: fromAddress,
creationTime: Math.floor(OTHelpers.now())
});
session.connections.add(fromConnection);
} else if (!fromConnection) {
logging.warn('OT.Raptor.dispatch: Messsage comes from a connection (' +
fromAddress + ') that we do not know about. The message was ignored.');
return;
}
actors.forEach(function(actor) {
actor.processMessage(method, fromConnection, message);
});
};
dispatcher.on('jsep#offer', jsepHandler.bind(null, 'offer'));
dispatcher.on('jsep#answer', jsepHandler.bind(null, 'answer'));
dispatcher.on('jsep#pranswer', jsepHandler.bind(null, 'pranswer'));
dispatcher.on('jsep#generateoffer', jsepHandler.bind(null, 'generateoffer'));
dispatcher.on('jsep#unsubscribe', jsepHandler.bind(null, 'unsubscribe'));
dispatcher.on('jsep#candidate', jsepHandler.bind(null, 'candidate'));
dispatcher.on('subscriberChannel#updated', function(streamId, channelId, content) {
if (!streamId || !session.streams.has(streamId)) {
logging.error('OT.Raptor.dispatch: Unable to determine streamId, or the stream does not ' +
'exist, for subscriberChannel#updated message!');
// @todo error
return;
}
session.streams.get(streamId)._
.updateChannel(channelId, content);
});
dispatcher.on('subscriberChannel#update', function(subscriberId, streamId, content) {
if (!streamId || !session.streams.has(streamId)) {
logging.error('OT.Raptor.dispatch: Unable to determine streamId, or the stream does not ' +
'exist, for subscriberChannel#update message!');
// @todo error
return;
}
// Hint to update for congestion control from the Media Server
if (!sessionObjects.subscribers.has(subscriberId)) {
logging.error('OT.Raptor.dispatch: Unable to determine subscriberId, or the subscriber ' +
'does not exist, for subscriberChannel#update message!');
// @todo error
return;
}
// We assume that an update on a Subscriber channel is to disableVideo
// we may need to be more specific in the future
sessionObjects.subscribers.get(subscriberId).disableVideo(content.active);
});
dispatcher.on('subscriber#created', function(streamId, fromAddress, subscriberId) {
var stream = streamId ? session.streams.get(streamId) : null;
if (!stream) {
logging.error('OT.Raptor.dispatch: Unable to determine streamId, or the stream does ' +
'not exist, for subscriber#created message!');
// @todo error
return;
}
session._.subscriberMap[fromAddress + '_' + stream.id] = subscriberId;
});
dispatcher.on('subscriber#deleted', function(streamId, fromAddress) {
var stream = streamId ? session.streams.get(streamId) : null;
if (!stream) {
logging.error('OT.Raptor.dispatch: Unable to determine streamId, or the stream does ' +
'not exist, for subscriber#created message!');
// @todo error
return;
}
delete session._.subscriberMap[fromAddress + '_' + stream.id];
});
dispatcher.on('signal', function(fromAddress, signalType, data) {
var fromConnection = session.connections.get(fromAddress);
if (session.connection && fromAddress === session.connection.connectionId) {
if (sessionStateReceived) {
session._.dispatchSignal(fromConnection, signalType, data);
} else {
delayedSessionEvents.enqueue('sessionConnected',
'signal', fromAddress, signalType, data);
}
} else if (session.connections.get(fromAddress)) {
session._.dispatchSignal(fromConnection, signalType, data);
} else if (fromAddress === '') { // Server originated signal
session._.dispatchSignal(null, signalType, data);
} else {
delayedSessionEvents.enqueue('connectionCreated' + fromAddress,
'signal', fromAddress, signalType, data);
}
});
dispatcher.on('archive#created', function(archive) {
parseAndAddArchiveToSession(archive, session);
});
dispatcher.on('archive#updated', function(archiveId, update) {
var archive = session.archives.get(archiveId);
if (!archive) {
logging.error('OT.Raptor.dispatch: An archive does not exist with the id of ' +
archiveId + ', for archive#updated message!');
// @todo error
return;
}
archive._.update(update);
});
return dispatcher;
};
},{"../../archive.js":169,"../../connection.js":180,"../../logging.js":187,"../../session/objects.js":239,"../../stream.js":241,"../../stream_channel.js":242,"./delayed_event_queue.js":188,"./dispatcher.js":190,"@opentok/ot-helpers":4}],198:[function(require,module,exports){
'use strict';
var APIKEY = require('../../api_key.js');
var Connection = require('../../connection.js');
var Message = require('./message.js');
var OTHelpers = require('@opentok/ot-helpers');
var sessionTag = require('../../session/tag.js');
var MAX_SIGNAL_DATA_LENGTH = 8192;
var MAX_SIGNAL_TYPE_LENGTH = 128;
//
// Error Codes:
// 413 - Type too long
// 400 - Type is invalid
// 413 - Data too long
// 400 - Data is invalid (can't be parsed as JSON)
// 429 - Rate limit exceeded
// 500 - Websocket connection is down
// 404 - To connection does not exist
// 400 - To is invalid
//
module.exports = function Signal(sessionId, fromConnectionId, options) {
var isInvalidType = function(type) {
// Our format matches the unreserved characters from the URI RFC:
// http://www.ietf.org/rfc/rfc3986
return !/^[a-zA-Z0-9\-\._~]+$/.exec(type);
};
var validateTo = function(toAddress) {
if (!toAddress) {
return {
code: 400,
reason: 'The signal to field was invalid. Either set it to a OT.Connection, ' +
'OT.Session, or omit it entirely'
};
}
if (!(toAddress instanceof Connection || toAddress._tag === sessionTag)) {
return {
code: 400,
reason: 'The To field was invalid'
};
}
return null;
};
var validateType = function(type) {
var error = null;
if (type === null || type === void 0) {
error = {
code: 400,
reason: 'The signal type was null or undefined. Either set it to a String value or ' +
'omit it'
};
} else if (type.length > MAX_SIGNAL_TYPE_LENGTH) {
error = {
code: 413,
reason: 'The signal type was too long, the maximum length of it is ' +
MAX_SIGNAL_TYPE_LENGTH + ' characters'
};
} else if (isInvalidType(type)) {
error = {
code: 400,
reason: 'The signal type was invalid, it can only contain letters, ' +
'numbers, \'-\', \'_\', and \'~\'.'
};
}
return error;
};
var validateData = function(data) {
var error = null;
if (data === null || data === void 0) {
error = {
code: 400,
reason: 'The signal data was null or undefined. Either set it to a String value or ' +
'omit it'
};
} else {
try {
if (JSON.stringify(data).length > MAX_SIGNAL_DATA_LENGTH) {
error = {
code: 413,
reason: 'The data field was too long, the maximum size of it is ' +
MAX_SIGNAL_DATA_LENGTH + ' characters'
};
}
} catch (e) {
error = { code: 400, reason: 'The data field was not valid JSON' };
}
}
return error;
};
var validateRetryAfterReconnect = function(retryAfterReconnect) {
var error = null;
if (!(retryAfterReconnect === true || retryAfterReconnect === false)) {
error = {
code: 400,
reason: 'The signal retryAfterReconnect was not true or false. Either set it to a Boolean ' +
'value or omit it'
};
}
return error;
};
this.toRaptorMessage = function() {
var to = this.to;
if (to && typeof to !== 'string') {
to = to.id;
}
return Message.signals.create(APIKEY.value, sessionId, to, this.type, this.data);
};
this.toHash = function() {
return options;
};
this.error = null;
this.retryAfterReconnect = true;
if (options) {
if (options.hasOwnProperty('data')) {
this.data = OTHelpers.clone(options.data);
this.error = validateData(this.data);
}
if (options.hasOwnProperty('to')) {
this.to = options.to;
if (!this.error) {
this.error = validateTo(this.to);
}
}
if (options.hasOwnProperty('type')) {
if (!this.error) {
this.error = validateType(options.type);
}
this.type = options.type;
}
if (options.hasOwnProperty('retryAfterReconnect')) {
if (!this.error) {
this.error = validateRetryAfterReconnect(options.retryAfterReconnect);
}
this.retryAfterReconnect = options.retryAfterReconnect;
}
}
this.valid = this.error === null;
};
},{"../../api_key.js":168,"../../connection.js":180,"../../session/tag.js":240,"./message.js":192,"@opentok/ot-helpers":4}],199:[function(require,module,exports){
'use strict';
var deserializeMessage = require('./deserialize_message.js');
module.exports = function unboxFromRumorMessage(rumorMessage) {
var message = deserializeMessage(rumorMessage.data);
message.transactionId = rumorMessage.transactionId;
message.fromAddress = rumorMessage.headers['X-TB-FROM-ADDRESS'];
return message;
};
},{"./deserialize_message.js":189}],200:[function(require,module,exports){
(function (global){
'use strict';
// Unfortunately it looks like the text-encoding module always returns its implementations of
// TextEncoder and TextDecoder, so we wrap it here to expose the global (window) ones, if available.
var TextEncoding = require('text-encoding');
module.exports = {
TextEncoder: global.TextEncoder || TextEncoding.TextEncoder,
TextDecoder: global.TextDecoder || TextEncoding.TextDecoder
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"text-encoding":133}],201:[function(require,module,exports){
'use strict';
var Rumor = {};
Rumor.MessageType = require('./rumor_message_types.js');
Rumor.PluginSocket = require('./plugin_socket.js');
Rumor.Message = require('./rumor_message.js');
Rumor.NativeSocket = require('./native_socket.js');
Rumor.SocketError = require('./socket_error.js');
Rumor.Socket = require('./rumor_socket.js');
module.exports = Rumor;
},{"./native_socket.js":202,"./plugin_socket.js":203,"./rumor_message.js":204,"./rumor_message_types.js":205,"./rumor_socket.js":206,"./socket_error.js":207}],202:[function(require,module,exports){
'use strict';
var logging = require('../../logging.js');
var Message = require('./rumor_message.js');
var BUFFER_DRAIN_INTERVAL = 100;
// The total number of times to retest the websocket's send buffer
var BUFFER_DRAIN_MAX_RETRIES = 10;
module.exports = function NativeSocket(TheWebSocket, messagingURL, events) {
var webSocket,
disconnectWhenSendBufferIsDrained,
bufferDrainTimeout, // Timer to poll whether th send buffer has been drained
close;
webSocket = new TheWebSocket(messagingURL);
webSocket.binaryType = 'arraybuffer';
webSocket.onopen = events.onOpen;
webSocket.onclose = events.onClose;
webSocket.onerror = events.onError;
webSocket.onmessage = function(message) {
if (!Message) {
// In IE 10/11, This can apparently be called after
// the page is unloaded and OT is garbage-collected
// TODO: To avoid a circular dependency, I've replaced !OT with !Message for the condition. If
// OT is garbage collected, surely Message would be too. We should try to figure out what's
// actually going on here, I find it hard to believe this is really what was happening. And
// even if it did, the result should be throwing an exception after the page unloads anyway,
// and that shouldn't matter.
return;
}
var msg = Message.deserialize(message.data);
events.onMessage(msg);
};
// Ensure that the WebSocket send buffer is fully drained before disconnecting
// the socket. If the buffer doesn't drain after a certain length of time
// we give up and close it anyway.
disconnectWhenSendBufferIsDrained = function disconnectWhenSendBufferIsDrained(bufferDrainRetries) {
if (!webSocket) { return; }
if (bufferDrainRetries === void 0) { bufferDrainRetries = 0; }
if (bufferDrainTimeout) { clearTimeout(bufferDrainTimeout); }
if (webSocket.bufferedAmount > 0 &&
(bufferDrainRetries + 1) <= BUFFER_DRAIN_MAX_RETRIES) {
bufferDrainTimeout = setTimeout(disconnectWhenSendBufferIsDrained,
BUFFER_DRAIN_INTERVAL, bufferDrainRetries + 1);
} else {
close();
}
};
close = function close() {
webSocket.close();
};
this.close = function(drainBuffer, removeEventHandlers) {
if (removeEventHandlers) {
var empty = function() {};
webSocket.onopen = empty;
webSocket.onclose = empty;
webSocket.onerror = empty;
webSocket.onmessage = empty;
}
if (drainBuffer) {
disconnectWhenSendBufferIsDrained();
} else {
close();
}
};
this.send = function(msg) {
try {
webSocket.send(msg.serialize());
} catch(err) {
if (webSocket.readyState === WebSocket.OPEN) {
// This is a known bug in Firefox:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1204727
logging.debug('webSocket.send threw exception even though it was open:', err);
} else {
throw err;
}
}
};
this.isClosed = function() {
return webSocket.readyState === 3;
};
};
},{"../../logging.js":187,"./rumor_message.js":204}],203:[function(require,module,exports){
'use strict';
var Message = require('./rumor_message.js');
var OTPlugin = require('@opentok/otplugin.js');
module.exports = function PluginSocket(messagingURL, events) {
var webSocket;
var state = 'initializing';
OTPlugin.initRumorSocket(messagingURL, function(err, rumorSocket) {
if (err) {
state = 'closed';
events.onClose({ code: 4999 });
} else if (state === 'initializing') {
webSocket = rumorSocket;
webSocket.onOpen(function() {
state = 'open';
events.onOpen();
});
webSocket.onClose(function(error) {
state = 'closed'; /* CLOSED */
events.onClose({ code: error });
});
webSocket.onError(function(error) {
state = 'closed'; /* CLOSED */
events.onError(error);
/* native websockets seem to do this, so should we */
events.onClose({ code: error });
});
webSocket.onMessage(function(type, addresses, headers, payload) {
var msg = new Message(type, addresses, headers, payload);
events.onMessage(msg);
});
webSocket.open();
} else {
this.close();
}
}.bind(this));
this.close = function(drainBuffer, removeEventHandlers) {
if (removeEventHandlers) {
var empty = function() {};
webSocket.onOpen(function() {
state = 'open';
empty();
});
webSocket.onClose(function() {
state = 'closed'; /* CLOSED */
empty();
});
webSocket.onError(function() {
state = 'closed'; /* CLOSED */
empty();
});
webSocket.onMessage(empty);
}
if (state === 'initializing' || state === 'closed') {
state = 'closed';
return;
}
webSocket.close(1000, '');
};
this.send = function(msg) {
if (state === 'open') {
webSocket.send(msg);
}
};
this.isClosed = function() {
return state === 'closed';
};
};
},{"./rumor_message.js":204,"@opentok/otplugin.js":40}],204:[function(require,module,exports){
(function (global){
'use strict';
var uuid = require('uuid');
var encoding = require('./encoding.js');
var RumorMessageTypes = require('./rumor_message_types.js');
//
//
// @references
// * https://tbwiki.tokbox.com/index.php/Rumor_Message_Packet
// * https://tbwiki.tokbox.com/index.php/Rumor_Protocol
//
var RumorMessage = function(type, toAddress, headers, data) {
this.type = type;
this.toAddress = toAddress;
this.headers = headers;
this.data = data;
this.fromAddress = this.headers['X-TB-FROM-ADDRESS'];
this.transactionId = this.headers['TRANSACTION-ID'];
this.status = this.headers.STATUS;
// Only status messages will have a status header. So a missing status header
// does not indicate an eror.
this.isError = this.status && this.status[0] !== '2';
};
module.exports = RumorMessage;
RumorMessage.prototype.serialize = function() {
var strArray, dataView, i, j;
var offset = 8;
var cBuf = 7;
var address = [];
var headerKey = [];
var headerVal = [];
// The number of addresses
cBuf++;
// Write out the address.
for (i = 0; i < this.toAddress.length; i++) {
/*jshint newcap:false */
address.push(new encoding.TextEncoder('utf-8').encode(this.toAddress[i]));
cBuf += 2;
cBuf += address[i].length;
}
// The number of parameters
cBuf++;
// Write out the params
i = 0;
for (var key in this.headers) { //eslint-disable-line one-var
if (!this.headers.hasOwnProperty(key)) {
continue;
}
headerKey.push(new encoding.TextEncoder('utf-8').encode(key));
headerVal.push(new encoding.TextEncoder('utf-8').encode(this.headers[key]));
cBuf += 4;
cBuf += headerKey[i].length;
cBuf += headerVal[i].length;
i++;
}
dataView = new encoding.TextEncoder('utf-8').encode(this.data);
cBuf += dataView.length;
// Let's allocate a binary blob of this size
var buffer = new ArrayBuffer(cBuf);
var uint8View = new Uint8Array(buffer, 0, cBuf);
// We don't include the header in the lenght.
cBuf -= 4;
// Write out size (in network order)
uint8View[0] = (cBuf & 0xFF000000) >>> 24;
uint8View[1] = (cBuf & 0x00FF0000) >>> 16;
uint8View[2] = (cBuf & 0x0000FF00) >>> 8;
uint8View[3] = (cBuf & 0x000000FF) >>> 0;
// Write out reserved bytes
uint8View[4] = 0;
uint8View[5] = 0;
// Write out message type
uint8View[6] = this.type;
uint8View[7] = this.toAddress.length;
// Now just copy over the encoded values..
for (i = 0; i < address.length; i++) {
strArray = address[i];
uint8View[offset++] = strArray.length >> 8 & 0xFF;
uint8View[offset++] = strArray.length >> 0 & 0xFF;
for (j = 0; j < strArray.length; j++) {
uint8View[offset++] = strArray[j];
}
}
uint8View[offset++] = headerKey.length;
// Write out the params
for (i = 0; i < headerKey.length; i++) {
strArray = headerKey[i];
uint8View[offset++] = strArray.length >> 8 & 0xFF;
uint8View[offset++] = strArray.length >> 0 & 0xFF;
for (j = 0; j < strArray.length; j++) {
uint8View[offset++] = strArray[j];
}
strArray = headerVal[i];
uint8View[offset++] = strArray.length >> 8 & 0xFF;
uint8View[offset++] = strArray.length >> 0 & 0xFF;
for (j = 0; j < strArray.length; j++) {
uint8View[offset++] = strArray[j];
}
}
// And finally the data
for (i = 0; i < dataView.length; i++) {
uint8View[offset++] = dataView[i];
}
return buffer;
};
function toArrayBuffer(buffer) {
var ab = new ArrayBuffer(buffer.length);
var view = new Uint8Array(ab);
for (var i = 0; i < buffer.length; ++i) {
view[i] = buffer[i];
}
return ab;
}
RumorMessage.deserialize = function(buffer) {
if (global.Buffer && global.Buffer.isBuffer(buffer)) {
buffer = toArrayBuffer(buffer);
}
var type, strView, headerlen, headers, keyStr, valStr, length, i;
var cBuf = 0;
var offset = 8;
var uint8View = new Uint8Array(buffer);
// Write out size (in network order)
cBuf += uint8View[0] << 24;
cBuf += uint8View[1] << 16;
cBuf += uint8View[2] << 8;
cBuf += uint8View[3] << 0;
type = uint8View[6];
var address = [];
for (i = 0; i < uint8View[7]; i++) {
length = uint8View[offset++] << 8;
length += uint8View[offset++];
strView = new Uint8Array(buffer, offset, length);
/*jshint newcap:false */
address[i] = new encoding.TextDecoder('utf-8').decode(strView);
offset += length;
}
headerlen = uint8View[offset++];
headers = {};
for (i = 0; i < headerlen; i++) {
length = uint8View[offset++] << 8;
length += uint8View[offset++];
strView = new Uint8Array(buffer, offset, length);
keyStr = new encoding.TextDecoder('utf-8').decode(strView);
offset += length;
length = uint8View[offset++] << 8;
length += uint8View[offset++];
strView = new Uint8Array(buffer, offset, length);
valStr = new encoding.TextDecoder('utf-8').decode(strView);
headers[keyStr] = valStr;
offset += length;
}
var dataView = new Uint8Array(buffer, offset);
var data = new encoding.TextDecoder('utf-8').decode(dataView);
return new RumorMessage(type, address, headers, data);
};
RumorMessage.Connect = function(uniqueId, notifyDisconnectAddress) {
var headers = {
uniqueId: uniqueId,
'TRANSACTION-ID': uuid(),
notifyDisconnectAddress: notifyDisconnectAddress
};
return new RumorMessage(RumorMessageTypes.CONNECT, [], headers, '');
};
RumorMessage.Disconnect = function(reconnect) {
return new RumorMessage(
RumorMessageTypes.DISCONNECT,
[],
{ reconnect: reconnect },
''
);
};
RumorMessage.Subscribe = function(topics) {
return new RumorMessage(RumorMessageTypes.SUBSCRIBE, topics, {}, '');
};
RumorMessage.Unsubscribe = function(topics) {
return new RumorMessage(RumorMessageTypes.UNSUBSCRIBE, topics, {}, '');
};
RumorMessage.Publish = function(topics, message, headers) {
return new RumorMessage(RumorMessageTypes.MESSAGE, topics, headers || {}, message || '');
};
RumorMessage.Status = function(topics, headers) {
return new RumorMessage(RumorMessageTypes.STATUS, topics, headers || {}, '');
};
// This message is used to implement keepalives on the persistent
// socket connection between the client and server. Every time the
// client sends a PING to the server, the server will respond with
// a PONG.
RumorMessage.Ping = function() {
return new RumorMessage(RumorMessageTypes.PING, [], {}, '');
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./encoding.js":200,"./rumor_message_types.js":205,"uuid":137}],205:[function(require,module,exports){
'use strict';
// Rumor Messaging for JS
//
// https://tbwiki.tokbox.com/index.php/Rumor_:_Messaging_FrameWork
//
// @todo Rumor {
// Add error codes for all the error cases
// Add Dependability commands
// }
module.exports = {
// This is used to subscribe to address/addresses. The address/addresses the
// client specifies here is registered on the server. Once any message is sent to
// that address/addresses, the client receives that message.
SUBSCRIBE: 0,
// This is used to unsubscribe to address / addresses. Once the client unsubscribe
// to an address, it will stop getting messages sent to that address.
UNSUBSCRIBE: 1,
// This is used to send messages to arbitrary address/ addresses. Messages can be
// anything and Rumor will not care about what is included.
MESSAGE: 2,
// This will be the first message that the client sends to the server. It includes
// the uniqueId for that client connection and a disconnect_notify address that will
// be notified once the client disconnects.
CONNECT: 3,
// This will be the message used by the server to notify an address that a
// client disconnected.
DISCONNECT: 4,
//Enhancements to support Keepalives
PING: 7,
PONG: 8,
STATUS: 9
};
},{}],206:[function(require,module,exports){
'use strict';
var uuid = require('uuid');
var isDOMUnloaded = require('../../../helpers/is_dom_unloaded.js');
var Message = require('./rumor_message.js');
var OTHelpers = require('@opentok/ot-helpers');
var logging = require('../../logging.js');
var NativeSocket = require('./native_socket.js');
var PluginSocket = require('./plugin_socket.js');
var RumorMessageTypes = require('./rumor_message_types.js');
var RumorMessage = require('./rumor_message.js');
var SocketError = require('./socket_error.js');
var ws = require('ws');
var WEB_SOCKET_KEEP_ALIVE_INTERVAL = 2000;
// Magic Connectivity Timeout Constant: We wait 2*the keep alive interval,
// on the third keep alive we trigger the timeout if we haven't received the
// server pong.
var WEB_SOCKET_CONNECTIVITY_TIMEOUT = 3 * WEB_SOCKET_KEEP_ALIVE_INTERVAL - 100;
var WEB_SOCKET_CONNECTIVITY_TIMEOUT_NO_RECONNECT = 25 * WEB_SOCKET_KEEP_ALIVE_INTERVAL - 100;
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Close_codes
// http://docs.oracle.com/javaee/7/api/javax/websocket/CloseReason.CloseCodes.html
var wsCloseErrorCodes = {
1002: 'The endpoint is terminating the connection due to a protocol error. ' +
'(CLOSE_PROTOCOL_ERROR)',
1003: 'The connection is being terminated because the endpoint has indicated ' +
'that reconnections are not available. (CLOSE_UNSUPPORTED)',
1004: 'The endpoint is terminating the connection because a data frame was received ' +
'that is too large. (CLOSE_TOO_LARGE)',
1005: 'Indicates that no status code was provided even though one was expected. ' +
'(CLOSE_NO_STATUS)',
1006: 'Used to indicate that a connection was closed abnormally (that is, with no ' +
'close frame being sent) when a status code is expected. (CLOSE_ABNORMAL)',
1007: 'Indicates that an endpoint is terminating the connection because it has received ' +
'data within a message that was not consistent with the type of the message (e.g., ' +
'non-UTF-8 [RFC3629] data within a text message)',
1008: 'Indicates that an endpoint is terminating the connection because it has received a ' +
'message that violates its policy. This is a generic status code that can be returned ' +
'when there is no other more suitable status code (e.g., 1003 or 1009) or if there is a ' +
'need to hide specific details about the policy',
1009: 'Indicates that an endpoint is terminating the connection because it has received a ' +
'message that is too big for it to process',
1011: 'Indicates that a server is terminating the connection because it encountered an ' +
'unexpected condition that prevented it from fulfilling the request',
// .... codes in the 4000-4999 range are available for use by applications.
4001: 'Connectivity loss was detected as it was too long since the socket received the ' +
'last PONG message',
4010: 'Timed out while waiting for the Rumor socket to connect.',
4020: 'Error code unavailable.',
4030: 'Exception was thrown during Rumor connection, possibly because of a blocked port.'
};
var errorMap = {
CLOSE_PROTOCOL_ERROR: 1002,
CLOSE_UNSUPPORTED: 1003,
CLOSE_TOO_LARGE: 1004,
CLOSE_NO_STATUS: 1005,
CLOSE_ABNORMAL: 1006,
CLOSE_UNEXPECTED_CONDITION: 1011,
CLOSE_TIMEOUT: 4010,
CLOSE_FALLBACK_CODE: 4020,
CLOSE_CONNECT_EXCEPTION: 4030
};
// The TheWebSocket bit is purely to make testing simpler, it defaults to WebSocket
// so in normal operation you would omit it.
var RumorSocket = function(options) {
var webSocket,
onOpen,
onError,
onClose,
onReconnecting,
onReconnectAttempt,
onReconnectFailure,
onReconnected,
onMessage,
connectCallback,
connectTimeout,
reconnectTimeout,
reconnectThreshold,
lastMessageTimestamp, // The timestamp of the last message received
keepAliveTimer; // Timer for the connectivity checks
var rumorSocket = this;
var reconnectAttempts = 0;
var states = ['disconnected', 'error', 'connected', 'connecting', 'disconnecting',
'reconnecting'];
var socketID = uuid();
var receivedTransactionIDs = [];
var messagingURL = options.messagingURL;
var notifyDisconnectAddress = options.notifyDisconnectAddress;
var connectionId = options.connectionId;
var enableReconnection = options.enableReconnection;
var TheWebSocket = options.TheWebSocket || ws;
var everConnected = false;
rumorSocket._connectivityTimeout = enableReconnection ? WEB_SOCKET_CONNECTIVITY_TIMEOUT :
WEB_SOCKET_CONNECTIVITY_TIMEOUT_NO_RECONNECT;
rumorSocket._keepAliveInterval = WEB_SOCKET_KEEP_ALIVE_INTERVAL;
// pendingMessages is pre-populated to connect to rumor by registering our connection id
// and the app server aQddress to notify if we disconnect, this will be sent on the first
// successful connection.
var pendingMessages = [RumorMessage.Connect(connectionId, notifyDisconnectAddress)];
//// Private API
var stateChanged = function(newState) {
switch (newState) {
case 'disconnected':
case 'error':
reconnectThreshold = undefined;
webSocket = null;
if (onClose) {
var error;
if (hasLostConnectivity()) {
error = new Error(wsCloseErrorCodes[4001]);
error.code = 4001;
}
onClose(error);
}
break;
case 'reconnecting':
if (onReconnecting && everConnected) {
onReconnecting();
}
if (rumorSocket.is('disconnected')) {
// This means some called disconnect in an onReconnecting
// handler, in that case we don't want to continue trying
// to reconnect.
return;
}
reconnectAttempts = 0;
reconnectThreshold = Date.now() + RumorSocket.RECONNECT_TIMEOUT;
break;
case 'connected':
if (onReconnected && everConnected) {
onReconnected();
} else {
everConnected = true;
}
reconnectThreshold = undefined;
break;
default:
}
};
var setState = OTHelpers.statable(rumorSocket, states, 'disconnected', stateChanged);
var validateCallback = function validateCallback(name, callback) {
if (callback === null || !OTHelpers.isFunction(callback)) {
throw new Error('The RumorSocket ' + name +
' callback must be a valid function or null');
}
};
var connectCallbackOnce = function() {
if (!connectCallback) {
return;
}
var args = Array.prototype.slice.call(arguments);
var callback = connectCallback;
connectCallback = void 0;
callback.apply(void 0, args);
};
var raiseError = function raiseError(code, extraDetail) {
code = code || errorMap.CLOSE_FALLBACK_CODE;
var messageFromCode = wsCloseErrorCodes[code] || 'No message available from code.';
var message = messageFromCode + (extraDetail ? ' ' + extraDetail : '');
logging.error('RumorSocket: ' + message);
var socketError = new SocketError(code, message);
clearTimeout(connectTimeout);
if (rumorSocket.is('reconnecting') && onReconnectFailure) {
onReconnectFailure(socketError);
}
var fatalErrorCode = code === errorMap.CLOSE_UNEXPECTED_CONDITION ||
code === errorMap.CLOSE_UNSUPPORTED;
if (rumorSocket.isNot('reconnecting', 'disconnecting', 'disconnected')) {
if (enableReconnection && !fatalErrorCode) {
logging.debug('Initial connectivity loss detected at ' + new Date());
setState('reconnecting');
} else {
logging.debug('Connectivity loss detected at ' + new Date());
}
}
if (rumorSocket.is('disconnected', 'disconnecting')) {
// This probably means someone one called disconnect in an onReconnecting
// handler, in that case we don't want to continue trying
// to reconnect.
return;
}
if (!enableReconnection || Date.now() >= reconnectThreshold || fatalErrorCode) {
if (fatalErrorCode) {
logging.debug('Connectivity not restored because of a fatal error.');
}
if (!enableReconnection) {
logging.debug('Reconnections are disabled, will not attempt to reconnect.');
} else if (Date.now() >= reconnectThreshold) {
logging.debug('Connectivity not restored within ' +
RumorSocket.RECONNECT_TIMEOUT + 'ms, we have disconnected.');
}
setState('error');
if (connectCallback) {
connectCallbackOnce(socketError, void 0);
} else if (onError) {
onError(socketError);
}
} else {
logging.debug('Scheduling reconnection in ' + RumorSocket.RECONNECT_RETRY + 'ms. ' +
'Remaining time: ' + (reconnectThreshold - Date.now()) + 'ms.');
reconnectTimeout = setTimeout(connect, RumorSocket.RECONNECT_RETRY);
reconnectAttempts++;
if (onReconnectAttempt) {
onReconnectAttempt();
}
}
};
var hasLostConnectivity = function hasLostConnectivity() {
if (!lastMessageTimestamp) { return false; }
return (OTHelpers.now() - lastMessageTimestamp) >= rumorSocket._connectivityTimeout;
};
var sendKeepAlive = function() {
if (!rumorSocket.is('connected')) { return; }
if (hasLostConnectivity()) {
webSocket.send(Message.Disconnect('1'));
webSocket.close(true, true);
webSocketDisconnected({ code: 4001 });
} else {
webSocket.send(Message.Ping());
keepAliveTimer = setTimeout(sendKeepAlive, rumorSocket._keepAliveInterval);
}
};
var sendAck = function(msg) {
webSocket.send(Message.Status([msg.fromAddress], {
'TRANSACTION-ID': msg.headers['TRANSACTION-ID'],
'X-TB-FROM-ADDRESS': connectionId
}));
};
//// Private Event Handlers
var webSocketConnected = function webSocketConnected() {
clearTimeout(connectTimeout);
if (rumorSocket.isNot('connecting', 'reconnecting')) {
logging.debug('webSocketConnected reached in state other than connecting (' +
rumorSocket.currentState + ')');
return;
}
logging.debug('Sending ' + pendingMessages.length + ' pending messages');
pendingMessages.map(function(message) {
webSocket.send(message);
});
pendingMessages = pendingMessages.filter(function(message) {
return message.type !== RumorMessageTypes.CONNECT;
});
setState('connected');
connectCallbackOnce(void 0, connectionId);
if (onOpen) {
onOpen(connectionId);
}
keepAliveTimer = setTimeout(function() {
lastMessageTimestamp = OTHelpers.now();
sendKeepAlive();
}, rumorSocket._keepAliveInterval);
};
var webSocketConnectTimedOut = function webSocketConnectTimedOut() {
var webSocketWas = webSocket;
raiseError(errorMap.CLOSE_TIMEOUT);
// This will prevent a socket eventually connecting
// But call it _after_ the error just in case any of
// the callbacks fire synchronously, breaking the error
// handling code.
try {
webSocketWas.close(false, true);
} catch (err) {
logging.debug('webSocket.close() raised an exception: ' + (err.message || err));
}
};
var webSocketError = function webSocketError() {};
var webSocketDisconnected = function webSocketDisconnected(closeEvent) {
logging.debug('OT.Rumor.Socket: webSocketDisconnected (code: ' + closeEvent.code + ')');
clearTimeout(connectTimeout);
clearTimeout(keepAliveTimer);
if (isDOMUnloaded()) {
// Sometimes we receive the web socket close event after
// the DOM has already been partially or fully unloaded
// if that's the case here then it's not really safe, or
// desirable, to continue.
return;
}
if (rumorSocket.isNot('disconnecting') && closeEvent.code !== 1000 && closeEvent.code !== 1001) {
if (closeEvent.code) {
raiseError(closeEvent.code);
} else {
raiseError(
errorMap.CLOSE_FALLBACK_CODE,
closeEvent.reason || closeEvent.message
);
}
}
if (rumorSocket.isNot('error', 'reconnecting')) {
setState('disconnected');
}
};
var webSocketReceivedMessage = function webSocketReceivedMessage(msg) {
lastMessageTimestamp = OTHelpers.now();
if (msg.type !== RumorMessageTypes.PONG) {
logging.debug('OT.Rumor.Socket webSocketReceivedMessage: ' +
JSON.stringify(msg, null, 2));
if (msg.transactionId) {
// remove pending message
pendingMessages = pendingMessages.filter(function(pendingMessage) {
if (pendingMessage.transactionId === msg.transactionId) {
logging.debug('Marking', msg.transactionId, ' as received');
}
return pendingMessage.transactionId !== msg.transactionId;
});
}
if (msg.transactionId && msg.type !== RumorMessageTypes.STATUS) {
// 1) ack it!
sendAck(msg);
// Have we seen this transaction before?
if (receivedTransactionIDs.indexOf(msg.transactionId) >= 0) {
// We've handled this transactionId before, but the ACK
// must have been lost. That's ok, we've told the server
// so we can just ignore this message now.
return;
}
receivedTransactionIDs.push(msg.transactionId);
}
if (onMessage) {
onMessage(msg);
}
}
};
var connect = function() {
if (rumorSocket.is('connecting', 'connected')) {
logging.error('Rumor.Socket cannot connect when it is already connecting or connected.');
return;
}
if (!rumorSocket.is('reconnecting')) {
setState('connecting');
} else {
logging.debug('Attempting reconnection...');
}
var attempt = uuid();
var events = {
onOpen: webSocketConnected,
onClose: webSocketDisconnected,
onError: webSocketError,
onMessage: webSocketReceivedMessage
};
var fullMessagingURL = messagingURL;
if (enableReconnection) {
fullMessagingURL = [
messagingURL,
messagingURL.indexOf('?') >= 0 ? '&' : '?',
'socketId=' + socketID,
rumorSocket.is('reconnecting') ? '&reconnect=true' : '',
'&attempt=' + attempt
].join('');
}
try {
if (TheWebSocket != null) {
webSocket = new NativeSocket(TheWebSocket, fullMessagingURL, events);
} else {
webSocket = new PluginSocket(fullMessagingURL, events);
}
connectTimeout = setTimeout(webSocketConnectTimedOut, RumorSocket.CONNECT_TIMEOUT);
} catch (e) {
logging.error(e);
raiseError(errorMap.CLOSE_CONNECT_EXCEPTION);
}
};
//// Public API
rumorSocket.publish = function(topics, message, headers, retryAfterReconnect) {
var rumorMessage = Message.Publish(topics, message, headers);
if (retryAfterReconnect) {
pendingMessages.push(rumorMessage);
}
if (rumorSocket.is('connected')) {
webSocket.send(rumorMessage);
}
};
rumorSocket.subscribe = function(topics) {
webSocket.send(Message.Subscribe(topics));
};
rumorSocket.unsubscribe = function(topics) {
webSocket.send(Message.Unsubscribe(topics));
};
rumorSocket.connect = function(complete) {
if (rumorSocket.is('connecting', 'connected')) {
if (typeof complete === 'function') {
complete(new SocketError(null,
'Rumor.Socket cannot connect when it is already connecting or connected.'));
} else {
logging.error('Rumor.Socket cannot connect when it is already connecting or connected.');
}
} else {
connectCallback = complete;
connect();
}
};
rumorSocket.disconnect = function(drainSocketBuffer) {
clearTimeout(connectTimeout);
clearTimeout(keepAliveTimer);
clearTimeout(reconnectTimeout);
if (!webSocket) {
if (rumorSocket.isNot('error')) { setState('disconnected'); }
return;
}
if (webSocket.isClosed()) {
if (rumorSocket.isNot('error')) { setState('disconnected'); }
} else {
if (rumorSocket.is('connected')) {
// Look! We are nice to the rumor server ;-)
webSocket.send(Message.Disconnect());
}
setState('disconnecting');
// Wait until the socket is ready to close
webSocket.close(drainSocketBuffer);
}
};
rumorSocket.status = function(toAddress, transactionId) {
webSocket.send(Message.Status(toAddress, {
'TRANSACTION-ID': transactionId,
'X-TB-FROM-ADDRESS': connectionId
}));
};
rumorSocket.reconnectRetriesCount = function() {
return reconnectAttempts;
};
rumorSocket.messageQueueSize = function() {
return pendingMessages.length;
};
OTHelpers.defineProperties(rumorSocket, {
id: {
get: function() { return connectionId; }
},
socketID: {
get: function() { return socketID; }
},
onOpen: {
set: function(callback) {
validateCallback('onOpen', callback);
onOpen = callback;
},
get: function() { return onOpen; }
},
onError: {
set: function(callback) {
validateCallback('onError', callback);
onError = callback;
},
get: function() { return onError; }
},
onClose: {
set: function(callback) {
validateCallback('onClose', callback);
onClose = callback;
},
get: function() { return onClose; }
},
onMessage: {
set: function(callback) {
validateCallback('onMessage', callback);
onMessage = callback;
},
get: function() { return onMessage; }
},
onReconnecting: {
set: function(callback) {
validateCallback('onReconnecting', callback);
onReconnecting = callback;
},
get: function() { return onReconnecting; }
},
onReconnectAttempt: {
set: function(callback) {
validateCallback('onReconnectAttempt', callback);
onReconnectAttempt = callback;
},
get: function() { return onReconnectAttempt; }
},
onReconnectFailure: {
set: function(callback) {
validateCallback('onReconnectFailure', callback);
onReconnectFailure = callback;
},
get: function() { return onReconnectFailure; }
},
onReconnected: {
set: function(callback) {
validateCallback('reconnected', callback);
onReconnected = callback;
},
get: function() {
return onReconnected;
}
}
});
};
// The number of ms to wait for the websocket to connect
RumorSocket.CONNECT_TIMEOUT = 15000;
RumorSocket.RECONNECT_TIMEOUT = 60000;
RumorSocket.RECONNECT_RETRY = 500;
module.exports = RumorSocket;
},{"../../../helpers/is_dom_unloaded.js":152,"../../logging.js":187,"./native_socket.js":202,"./plugin_socket.js":203,"./rumor_message.js":204,"./rumor_message_types.js":205,"./socket_error.js":207,"@opentok/ot-helpers":4,"uuid":137,"ws":138}],207:[function(require,module,exports){
'use strict';
module.exports = function(code, message) {
this.code = code;
this.message = message;
};
},{}],208:[function(require,module,exports){
'use strict';
var analytics = require('./analytics.js');
var APIKEY = require('./api_key.js');
var Events = require('./events.js');
var OTHelpers = require('@opentok/ot-helpers');
var logging = require('./logging.js');
/**
* The Error class is used to define the error object passed into completion handlers.
* Each of the following methods, which execute asynchronously, includes a
* completionHandler parameter:
*
*
* The completionHandler parameter is a function that is called when the call to
* the asynchronous method succeeds or fails. If the asynchronous call fails, the completion
* handler function is passes an error object (defined by the Error class). The code
* and message properties of the error object provide details about the error.
*
* @property {Number} code The error code, defining the error.
*
*
* In the event of an error, the code value of the error parameter can
* have one of the following values:
*
Errors when calling Session.connect():
code |
* Description | *
| 1004 | *Authentication error. Check the error message for details. This error can result if you * in an expired token when trying to connect to a session. It can also occur if you pass * in an invalid token or API key. Make sure that you are generating the token using the * current version of one of the * OpenTok server SDKs. | *
| 1005 | *Invalid Session ID. Make sure you generate the session ID using the current version of * one of the OpenTok server * SDKs. | *
| 1006 | *Connect Failed. Unable to connect to the session. You may want to have the client check * the network connection. | *
| 1026 | *Terms of service violation: export compliance. See the * Terms of Service. | *
| 2001 | *Connect Failed. Unexpected response from the OpenTok server. Try connecting again * later. | *
Errors when calling Session.forceDisconnect():
* code
* |
* Description | *
| 1010 | *The client is not connected to the OpenTok session. Check that client connects * successfully and has not disconnected before calling forceDisconnect(). | *
| 1520 | *Unable to force disconnect. The client's token does not have the role set to moderator.
* Once the client has connected to the session, the capabilities property of
* the Session object lists the client's capabilities. |
*
Errors when calling Session.forceUnpublish():
code |
* Description | *
| 1010 | *The client is not connected to the OpenTok session. Check that client connects * successfully and has not disconnected before calling forceUnpublish(). | *
| 1530 | *Unable to force unpublish. The client's token does not have the role set to moderator.
* Once the client has connected to the session, the capabilities property of
* the Session object lists the client's capabilities. |
*
| 1535 | *Force Unpublish on an invalid stream. Make sure that the stream has not left the
* session before you call the forceUnpublish() method. |
*
Errors when calling Session.publish():
code |
* Description | *
| 1010 | *The client is not connected to the OpenTok session. Check that the client connects * successfully before trying to publish. And check that the client has not disconnected * before trying to publish. | *
| 1500 | *Unable to Publish. The client's token does not have the role set to to publish or
* moderator. Once the client has connected to the session, the capabilities
* property of the Session object lists the client's capabilities. |
*
| 1553 | *WebRTC ICE workflow error. Try publishing again or reconnecting to the session. | *
| 1601 | *Internal error -- WebRTC publisher error. Try republishing or reconnecting to the * session. | *
| 2001 | *Publish Failed. Unexpected response from the OpenTok server. Try publishing again * later. | *
Errors when calling Session.signal():
code |
* Description | *
| 400 | *One of the signal properties — data, type, or to — * is invalid. Or the data cannot be parsed as JSON. | *
| 404 | The to connection does not exist. | *
| 413 | The type string exceeds the maximum length (128 bytes), * or the data string exceeds the maximum size (8 kB). | *
| 500 | *The client is not connected to the OpenTok session. Check that the client connects * successfully before trying to signal. And check that the client has not disconnected before * trying to publish. | *
| 2001 | *Signal Failed. Unexpected response from the OpenTok server. Try sending the signal again * later. | *
Errors when calling Session.subscribe():
* code
* |
* Description | *
| 1013 | *WebRTC PeerConnection error. Try resubscribing to the stream or * reconnecting to the session. | *
| 1554 | *WebRTC ICE workflow error. Try resubscribing to the stream or * reconnecting to the session. | *
| 1600 | *Internal error -- WebRTC subscriber error. Try resubscribing to the stream or * reconnecting to the session. | *
| 2001 | *Subscribe Failed. Unexpected response from the OpenTok server. Try subscribing again * later. | *
Errors when calling OT.initPublisher():
code |
* Description | *
| 1004 | *Authentication error. Check the error message for details. This error can result if you * pass in an expired token when trying to connect to a session. It can also occur if you * pass in an invalid token or API key. Make sure that you are generating the token using * the current version of one of the * OpenTok server SDKs. | *
| 1550 | *Screen sharing is not supported (and you set the videoSource property
* of the options parameter of OT.initPublisher() to
* "application", "screen", or "window").
* Before calling OT.initPublisher(), you can call
* OT.checkScreenSharingCapability()
* to check if screen sharing is supported. |
*
| 1551 | *A screen-sharing extension needs to be registered but it is not. This error can occur
* when you set the videoSource property of the options parameter
* of OT.initPublisher() to "application", "screen",
* or "window". Before calling OT.initPublisher(), you can call
* OT.checkScreenSharingCapability()
* to check if screen sharing requires an extension to be registered. |
*
| 1552 | *A screen-sharing extension is required, but it is not installed. This error can occur
* when you set the videoSource property of the options parameter
* of OT.initPublisher() to "screen". Before calling
* OT.initPublisher(), you can call
* OT.checkScreenSharingCapability()
* to check if screen sharing requires an extension to be installed. |
*
Errors when calling OT.initPublisher():
code |
* Description | *
| 2011 | *Error calling OT.reportIssue(). Check the client's network connection. | *
General errors that can occur when calling any method:
* *code |
* Description | *
| 1011 | *Invalid Parameter. Check that you have passed valid parameter values into the method * call. | *
| 2000 | *Internal Error. Try reconnecting to the OpenTok session and trying the action again. | *
OT.initPublisher() method
* creates a Publisher object.
*
* The following code instantiates a session, and publishes an audio-video stream * upon connection to the session:
* *
* var apiKey = ''; // Replace with your API key. See https://dashboard.tokbox.com/projects
* var sessionID = ''; // Replace with your own session ID.
* // See https://dashboard.tokbox.com/projects
* var token = ''; // Replace with a generated token that has been assigned the moderator role.
* // See https://dashboard.tokbox.com/projects
*
* var session = OT.initSession(apiKey, sessionID);
* session.connect(token, function(error) {
* if (error) {
* console.log(error.message);
* } else {
* // This example assumes that a DOM element with the ID 'publisherElement' exists
* var publisherProperties = {width: 400, height:300, name:"Bob's stream"};
* publisher = OT.initPublisher('publisherElement', publisherProperties);
* session.publish(publisher);
* }
* });
*
*
* This example creates a Publisher object and adds its video to a DOM element
* with the ID publisherElement by calling the OT.initPublisher()
* method. It then publishes a stream to the session by calling
* the publish() method of the Session object.
accessAllowed event when
* the user grants access. The Publisher object dispatches an accessDenied event
* when the user denies access.
* @property {Element} element The HTML DOM element containing the Publisher. (Note:
* when you set the insertDefaultUI option to false in the call to
* OT.initPublisher, the element property
* is undefined.)
* @property {String} id The DOM ID of the Publisher.
* @property {Stream} stream The {@link Stream} object corresponding the the stream of
* the Publisher.
* @property {Session} session The {@link Session} to which the Publisher belongs.
*
* @see OT.initPublisher
* @see Session.publish()
*
* @class Publisher
* @augments EventDispatcher
*/
var Publisher = function(options) {
// Check that the client meets the minimum requirements, if they don't the upgrade
// flow will be triggered.
if (!systemRequirements.check()) {
systemRequirements.upgrade();
return;
}
var _widgetView,
_videoElementFacade,
_stream,
_streamId,
_webRTCStream,
_session,
_publishStartTime,
_microphone,
_chrome,
_audioLevelMeter,
_properties,
_validResolutions,
_state,
_iceServers,
_connectivityAttemptPinger,
_attemptStartTime,
_enableSimulcast,
_audioDevices,
_videoDevices,
_selectedVideoInputDeviceId,
_selectedAudioInputDeviceId;
var _guid = Publisher.nextId();
var _peerConnections = {};
var _loaded = false;
var _validFrameRates = [1, 7, 15, 30];
var _isScreenSharing = options && (
options.videoSource === 'screen' ||
options.videoSource === 'window' ||
options.videoSource === 'tab' ||
options.videoSource === 'browser' ||
options.videoSource === 'application'
);
var _previousAnalyticsStats = {};
var self = this;
_properties = OTHelpers.defaults(options || {}, {
publishAudio: _isScreenSharing ? false : true,
publishVideo: true,
mirror: _isScreenSharing ? false : true,
showControls: true,
fitMode: _isScreenSharing ? 'contain' : 'cover',
audioFallbackEnabled: _isScreenSharing ? false : true,
maxResolution: _isScreenSharing ? { width: 1920, height: 1920 } : undefined,
insertDefaultUI: true
});
_validResolutions = {
'320x240': { width: 320, height: 240 },
'320x180': { width: 320, height: 180 },
'640x480': { width: 640, height: 480 },
'640x360': { width: 640, height: 360 },
'1280x720': { width: 1280, height: 720 },
'1280x960': { width: 1280, height: 960 }
};
OTHelpers.eventing(this);
if (!_isScreenSharing) {
var audioLevelRunner = new IntervalRunner(function() {
if (_videoElementFacade) {
_videoElementFacade.getAudioInputLevel()
.then(function(audioInputLevel) {
OTHelpers.requestAnimationFrame(function() {
self.dispatchEvent(
new Events.AudioLevelUpdatedEvent(audioInputLevel));
});
});
}
}, 60);
this.on({
'audioLevelUpdated:added': function(count) {
if (count === 1) {
audioLevelRunner.start();
}
},
'audioLevelUpdated:removed': function(count) {
if (count === 0) {
audioLevelRunner.stop();
}
}
});
}
/// Private Methods
var logAnalyticsEvent = function(action, variation, payload, options, throttle) {
var stats = OTHelpers.extend({
action: action,
variation: variation,
payload: payload,
sessionId: _session ? _session.sessionId : null,
connectionId: _session && _session.isConnected() ? _session.connection.connectionId : null,
partnerId: _session ? _session.apiKey : APIKEY.value,
p2p: _session && _session.sessionInfo ? _session.sessionInfo.p2pEnabled : null,
messagingServer: (_session && _session.sessionInfo) ? _session.sessionInfo.messagingServer :
null,
streamId: _streamId
}, options);
if (variation === 'Failure') {
stats = OTHelpers.extend(_previousAnalyticsStats, stats);
}
_previousAnalyticsStats = pick(stats, 'sessionId', 'connectionId', 'partnerId');
analytics.logEvent(stats, throttle);
};
var logConnectivityEvent = function(variation, payload, options) {
if (variation === 'Attempt' || !_connectivityAttemptPinger) {
_attemptStartTime = new Date().getTime();
_connectivityAttemptPinger = new ConnectivityAttemptPinger({
action: 'Publish',
sessionId: _session ? _session.sessionId : null,
connectionId: _session &&
_session.isConnected() ? _session.connection.connectionId : null,
partnerId: _session ? _session.apiKey : APIKEY.value,
p2p: _session && _session.sessionInfo ? _session.sessionInfo.p2pEnabled : null,
messagingServer: (_session && _session.sessionInfo) ? _session.sessionInfo.messagingServer :
null,
streamId: _streamId
});
}
if (!options || options.failureReason !== 'Non-fatal') {
if (variation === 'Failure') {
// We don't want to log an invalid sequence in this case because it was a
// non-fatal failure
_connectivityAttemptPinger.setVariation(variation);
}
if (variation === 'Failure' || variation === 'Success' || variation === 'Cancel') {
if (!options) { options = {}; }
OTHelpers.extend(options, {
attemptDuration: new Date().getTime() - _attemptStartTime
});
payload = payload || {};
OTHelpers.extend(payload, {
videoInputDevices: _videoDevices,
audioInputDevices: _audioDevices,
videoInputDeviceCount: _videoDevices ? _videoDevices.length : undefined,
audioInputDeviceCount: _audioDevices ? _audioDevices.length : undefined,
selectedVideoInputDeviceId: _selectedVideoInputDeviceId,
selectedAudioInputDeviceId: _selectedAudioInputDeviceId
});
}
logAnalyticsEvent('Publish', variation, payload, options);
}
};
var logRepublish = function(variation, payload) {
logAnalyticsEvent('ICERestart', variation, payload);
};
var recordQOS = function(connection, parsedStats) {
var domElement;
if (_widgetView && _widgetView.domElement) {
domElement = _widgetView.domElement;
} else if (_widgetView && _widgetView.video() && _widgetView.video().domElement()) {
// If we're using insertDefaultUI=false then there is no container
domElement = _widgetView.video().domElement();
}
var QoSBlob = {
widgetType: 'Publisher',
sessionId: _session ? _session.sessionId : null,
connectionId: _session && _session.isConnected() ?
_session.connection.connectionId : null,
partnerId: _session ? _session.apiKey : APIKEY.value,
streamId: _streamId,
width: domElement ? Number(OTHelpers.width(domElement).replace('px', ''))
: undefined,
height: domElement ? Number(OTHelpers.height(domElement).replace('px', ''))
: undefined,
audioTrack: _webRTCStream && _webRTCStream.getAudioTracks().length > 0,
hasAudio: _properties.publishAudio,
videoTrack: _webRTCStream && _webRTCStream.getVideoTracks().length > 0,
hasVideo: _properties.publishVideo,
videoSource: _isScreenSharing && options.videoSource ||
_properties.constraints.video && 'Camera' || null,
version: properties.version,
mediaServerName: _session ? _session.sessionInfo.mediaServerName : null,
apiServer: properties.apiURL,
p2p: _session ? _session.sessionInfo.p2pEnabled : null,
messagingServer: _session ? _session.sessionInfo.messagingServer : null,
duration: _publishStartTime ?
Math.round((new Date().getTime() - _publishStartTime.getTime()) / 1000) : 0,
remoteConnectionId: connection.id,
scalableVideo: !!_enableSimulcast
};
analytics.logQOS(OTHelpers.extend(QoSBlob, parsedStats));
self.trigger('qos', parsedStats);
};
// Returns the video dimensions. Which could either be the ones that
// the developer specific in the videoDimensions property, or just
// whatever the video element reports.
//
// If all else fails then we'll just default to 640x480
//
var getVideoDimensions = function() {
var streamWidth, streamHeight;
// We set the streamWidth and streamHeight to be the minimum of the requested
// resolution and the actual resolution.
if (_properties.videoDimensions) {
streamWidth = Math.min(_properties.videoDimensions.width,
(_videoElementFacade && _videoElementFacade.videoWidth()) || 640);
streamHeight = Math.min(_properties.videoDimensions.height,
(_videoElementFacade && _videoElementFacade.videoHeight()) || 480);
} else {
streamWidth = (_videoElementFacade && _videoElementFacade.videoWidth()) || 640;
streamHeight = (_videoElementFacade && _videoElementFacade.videoHeight()) || 480;
}
return {
width: streamWidth,
height: streamHeight
};
};
/// Private Events
var stateChangeFailed = function(changeFailed) {
logging.error('OT.Publisher State Change Failed: ', changeFailed.message);
logging.debug(changeFailed);
};
var onLoaded = function() {
if (_state.isDestroyed()) {
// The publisher was destroyed before loading finished
return;
}
logging.debug('OT.Publisher.onLoaded');
_state.set('MediaBound');
// If we have a session and we haven't created the stream yet then
// wait until that is complete before hiding the loading spinner
_widgetView.loading(self.session ? !_stream : false);
_loaded = true;
if (self.element) {
// Only create the chrome if we have an element to insert it into
// for insertDefautlUI:false we don't create the chrome
createChrome.call(self);
}
self.trigger('initSuccess');
self.trigger('loaded', self);
};
var onLoadFailure = function(reason) {
var errorCode = ExceptionCodes.P2P_CONNECTION_FAILED;
var options = {
failureReason: 'PeerConnectionError',
failureCode: errorCode,
failureMessage: reason
};
logConnectivityEvent('Failure', null, options);
_state.set('Failed');
self.trigger('publishComplete', new OTError(errorCode,
'OT.Publisher PeerConnection Error: ' + reason));
OTError.handleJsException(
'OT.Publisher PeerConnection Error: ' + reason,
ExceptionCodes.P2P_CONNECTION_FAILED,
{
session: _session,
target: self
}
);
};
// Clean up our LocalMediaStream
var cleanupLocalStream = function() {
if (_webRTCStream) {
// Stop revokes our access cam and mic access for this instance
// of localMediaStream.
if (global.MediaStreamTrack && global.MediaStreamTrack.prototype.stop) {
// Newer spec
_webRTCStream.getTracks()
.forEach(function(track) { track.stop(); });
} else {
// Older spec
_webRTCStream.stop();
}
_webRTCStream = null;
}
};
var onStreamAvailable = function(webOTStream) {
logging.debug('OT.Publisher.onStreamAvailable');
_state.set('BindingMedia');
cleanupLocalStream();
_webRTCStream = webOTStream;
var findSelectedDeviceId = function(tracks, devices) {
// Store the device labels to log later
var selectedDeviceId;
tracks.forEach(function(track) {
if (track.deviceId) {
// IE adds a deviceId property to the track
selectedDeviceId = track.deviceId;
} else if (track.label && devices) {
var selectedDevice = OTHelpers.find(devices, function(el) {
return el.label === track.label;
});
if (selectedDevice) {
selectedDeviceId = selectedDevice.deviceId;
}
}
});
return selectedDeviceId;
};
_selectedVideoInputDeviceId = findSelectedDeviceId(_webRTCStream.getVideoTracks(),
_videoDevices);
_selectedAudioInputDeviceId = findSelectedDeviceId(_webRTCStream.getAudioTracks(),
_audioDevices);
_microphone = new Publisher.Microphone(_webRTCStream, !_properties.publishAudio);
self.publishVideo(_properties.publishVideo &&
_webRTCStream.getVideoTracks().length > 0);
self.accessAllowed = true;
self.dispatchEvent(new Events.Event(Events.Event.names.ACCESS_ALLOWED, false));
var videoContainerOptions = {
muted: true,
error: onVideoError
};
_videoElementFacade = _widgetView.bindVideo(_webRTCStream,
videoContainerOptions,
function(err) {
if (err) {
onLoadFailure(err);
return;
}
onLoaded();
});
};
var onPublishingTimeout = function(session) {
logging.error('OT.Publisher.onPublishingTimeout');
_state.set('Failed');
self.trigger('publishComplete', new OTError(ExceptionCodes.UNABLE_TO_PUBLISH,
'Could not publish in a reasonable amount of time'));
if (session.isConnected() && self.stream) {
session._.streamDestroy(self.stream.id);
}
// Disconnect immediately, rather than wait for the WebSocket to
// reply to our destroyStream message.
self.disconnect();
self.session = _session = null;
// We're back to being a stand-alone publisher again.
if (!_state.isDestroyed()) { _state.set('MediaBound'); }
if (_connectivityAttemptPinger) {
_connectivityAttemptPinger.stop();
_connectivityAttemptPinger = null;
}
self._.streamDestroyed('networkDisconnected');
var options = {
failureReason: 'ICEWorkflow',
failureCode: ExceptionCodes.UNABLE_TO_PUBLISH,
failureMessage: 'OT.Publisher failed to publish in a reasonable amount of time (timeout)'
};
logConnectivityEvent('Failure', null, options);
OTError.handleJsException(
options.failureReason,
options.failureCode,
{
session: _session,
target: self
}
);
};
var onStreamAvailableError = function(error) {
logging.error('OT.Publisher.onStreamAvailableError ' + error.name + ': ' + error.message);
_state.set('Failed');
self.trigger('publishComplete', new OTError(ExceptionCodes.UNABLE_TO_PUBLISH,
error.message));
if (_widgetView) { _widgetView.destroy(); }
var options = {
failureReason: 'GetUserMedia',
failureCode: ExceptionCodes.UNABLE_TO_PUBLISH,
failureMessage: 'OT.Publisher failed to access camera/mic: ' + error.message
};
logConnectivityEvent('Failure', null, options);
OTError.handleJsException(
options.failureReason,
options.failureCode,
{
session: _session,
target: self
}
);
};
var onScreenSharingError = function(error) {
logging.error('OT.Publisher.onScreenSharingError ' + error.message);
_state.set('Failed');
self.trigger('publishComplete', new OTError(ExceptionCodes.UNABLE_TO_PUBLISH,
'Screensharing: ' + error.message));
var options = {
failureReason: 'ScreenSharing',
failureMessage: error.message
};
logConnectivityEvent('Failure', null, options);
};
// The user has clicked the 'deny' button the the allow access dialog
// (or it's set to always deny)
var onAccessDenied = function(error) {
if (_isScreenSharing) {
if (global.location.protocol !== 'https:') {
/**
* in http:// the browser will deny permission without asking the
* user. There is also no way to tell if it was denied by the
* user, or prevented from the browser.
*/
error.message += ' Note: https:// is required for screen sharing.';
}
}
logging.error('OT.Publisher.onStreamAvailableError Permission Denied');
_state.set('Failed');
var errorMessage = 'OT.Publisher Access Denied: Permission Denied' +
(error.message ? ': ' + error.message : '');
var errorCode = ExceptionCodes.UNABLE_TO_PUBLISH;
self.trigger('publishComplete', new OTError(errorCode, errorMessage));
var payload = {
reason: 'AccessDenied'
};
logConnectivityEvent('Cancel', payload);
self.dispatchEvent(new Events.Event(Events.Event.names.ACCESS_DENIED));
};
var onAccessDialogOpened = function() {
logAnalyticsEvent('accessDialog', 'Opened');
self.dispatchEvent(new Events.Event(Events.Event.names.ACCESS_DIALOG_OPENED, true));
};
var onAccessDialogClosed = function() {
logAnalyticsEvent('accessDialog', 'Closed');
self.dispatchEvent(new Events.Event(Events.Event.names.ACCESS_DIALOG_CLOSED, false));
};
var onVideoError = function(errorCode, errorReason) {
logging.error('OT.Publisher.onVideoError');
var message = errorReason + (errorCode ? ' (' + errorCode + ')' : '');
logAnalyticsEvent('stream', null, { reason: 'OT.Publisher while playing stream: ' + message });
_state.set('Failed');
if (_state.isAttemptingToPublish()) {
self.trigger('publishComplete', new OTError(ExceptionCodes.UNABLE_TO_PUBLISH,
message));
} else {
self.trigger('error', message);
}
OTError.handleJsException('OT.Publisher error playing stream: ' + message,
ExceptionCodes.UNABLE_TO_PUBLISH, {
session: _session,
target: self
});
};
var onPeerDisconnected = function(peerConnection) {
logging.debug('Subscriber has been disconnected from the Publisher\'s PeerConnection');
self.cleanupSubscriber(peerConnection.remoteConnection().id);
};
var onPeerConnectionFailure = function(code, reason, peerConnection, prefix) {
if (prefix === 'ICEWorkflow' && _session.sessionInfo.reconnection && _loaded) {
logging.debug('Ignoring peer connection failure due to possibility of reconnection.');
return;
}
var errorCode;
if (prefix === 'ICEWorkflow') {
errorCode = ExceptionCodes.PUBLISHER_ICE_WORKFLOW_FAILED;
} else {
errorCode = ExceptionCodes.UNABLE_TO_PUBLISH;
}
var payload = {
hasRelayCandidates: peerConnection.hasRelayCandidates()
};
var options = {
failureReason: prefix ? prefix : 'PeerConnectionError',
failureCode: errorCode,
failureMessage: (prefix ? prefix : '') + ':Publisher PeerConnection with connection ' +
(peerConnection && peerConnection.remoteConnection &&
peerConnection.remoteConnection().id) + ' failed: ' + reason
};
if (_state.isPublishing()) {
// We're already publishing so this is a Non-fatal failure, must be p2p and one of our
// peerconnections failed
options.reason = 'Non-fatal';
} else {
self.trigger('publishComplete', new OTError(ExceptionCodes.UNABLE_TO_PUBLISH,
payload.message));
}
logConnectivityEvent('Failure', payload, options);
OTError.handleJsException('OT.Publisher PeerConnection Error: ' + reason, errorCode, {
session: _session,
target: self
});
// We don't call cleanupSubscriber as it also logs a
// disconnected analytics event, which we don't want in this
// instance. The duplication is crufty though and should
// be tidied up.
delete _peerConnections[peerConnection.remoteConnection().id];
};
var onIceRestartSuccess = function(connectionId) {
logRepublish('Success', { remoteConnectionId: connectionId });
};
var onIceRestartFailure = function(connectionId) {
logRepublish('Failure', {
reason: 'ICEWorkflow',
message: 'OT.Publisher PeerConnection Error: ' +
'The stream was unable to connect due to a network error.' +
' Make sure your connection isn\'t blocked by a firewall.',
remoteConnectionId: connectionId
});
};
/// Private Helpers
// Assigns +stream+ to this publisher. The publisher listens
// for a bunch of events on the stream so it can respond to
// changes.
var assignStream = function(stream) {
self.stream = _stream = stream;
_stream.on('destroyed', self.disconnect, self);
_state.set('Publishing');
_widgetView.loading(!_loaded);
_publishStartTime = new Date();
self.trigger('publishComplete', null, self);
self.dispatchEvent(new Events.StreamEvent('streamCreated', stream, null, false));
logConnectivityEvent('Success');
};
var createPeerConnectionForRemote = function(remoteConnection, uri, completion) {
var peerConnection = _peerConnections[remoteConnection.id];
if (!peerConnection) {
var startConnectingTime = OTHelpers.now();
logAnalyticsEvent('createPeerConnection', 'Attempt');
// Cleanup our subscriber when they disconnect
remoteConnection.on('destroyed',
self.cleanupSubscriber.bind(self, remoteConnection.id));
// Calculate the number of streams to use. 1 for normal, >1 for Simulcast
var numberOfSimulcastStreams = 1;
_enableSimulcast = false;
if (OTHelpers.env.name === 'Chrome' && !_isScreenSharing &&
!_session.sessionInfo.p2pEnabled &&
_properties.constraints.video) {
// We only support simulcast on Chrome, and when not using
// screensharing.
_enableSimulcast = _session.sessionInfo.simulcast;
if (_enableSimulcast === void 0) {
// If there is no session wide preference then allow the
// developer to choose.
_enableSimulcast = options && options._enableSimulcast;
}
}
if (_enableSimulcast) {
var streamDimensions = getVideoDimensions();
// HD and above gets three streams. Otherwise they get 2.
if (streamDimensions.width > 640 &&
streamDimensions.height > 480) {
numberOfSimulcastStreams = 3;
} else {
numberOfSimulcastStreams = 2;
}
}
peerConnection = _peerConnections[remoteConnection.id] = (
new Publisher.PublisherPeerConnection(
remoteConnection,
_session,
_streamId,
_webRTCStream,
_properties.channels,
numberOfSimulcastStreams,
uri,
logAnalyticsEvent
)
);
peerConnection.on({
connected: function() {
var payload = {
pcc: parseInt(OTHelpers.now() - startConnectingTime, 10),
hasRelayCandidates: peerConnection.hasRelayCandidates()
};
logAnalyticsEvent('createPeerConnection', 'Success', payload);
},
disconnected: onPeerDisconnected,
error: onPeerConnectionFailure,
qos: recordQOS,
iceRestartSuccess: onIceRestartSuccess.bind(undefined, remoteConnection.id),
iceRestartFailure: onIceRestartFailure.bind(undefined, remoteConnection.id)
});
peerConnection.init(_iceServers, completion);
} else {
OTHelpers.callAsync(function() {
completion(undefined, peerConnection);
});
}
};
/// Chrome
// If mode is false, then that is the mode. If mode is true then we'll
// definitely display the button, but we'll defer the model to the
// Publishers buttonDisplayMode style property.
var chromeButtonMode = function(mode) {
if (mode === false) { return 'off'; }
var defaultMode = self.getStyle('buttonDisplayMode');
// The default model is false, but it's overridden by +mode+ being true
if (defaultMode === false) { return 'on'; }
// defaultMode is either true or auto.
return defaultMode;
};
var updateChromeForStyleChange = function(key, value) {
if (!_chrome) { return; }
switch (key) {
case 'nameDisplayMode':
_chrome.name.setDisplayMode(value);
_chrome.backingBar.setNameMode(value);
break;
case 'showArchiveStatus':
logAnalyticsEvent('showArchiveStatus', 'styleChange', { mode: value ? 'on' : 'off' });
_chrome.archive.setShowArchiveStatus(value);
break;
case 'archiveStatusDisplayMode':
_chrome.archive.setShowArchiveStatus(value !== 'off');
break;
case 'buttonDisplayMode':
_chrome.muteButton.setDisplayMode(value);
_chrome.backingBar.setMuteMode(value);
break;
case 'audioLevelDisplayMode':
_chrome.audioLevel.setDisplayMode(value);
break;
case 'backgroundImageURI':
_widgetView.setBackgroundImageURI(value);
break;
default:
}
};
var createChrome = function() {
if (!self.getStyle('showArchiveStatus')) {
logAnalyticsEvent('showArchiveStatus', 'createChrome', { mode: 'off' });
}
var widgets = {
backingBar: new BackingBar({
nameMode: !_properties.name ? 'off' : self.getStyle('nameDisplayMode'),
muteMode: chromeButtonMode.call(self, self.getStyle('buttonDisplayMode'))
}),
name: new NamePanel({
name: _properties.name,
mode: self.getStyle('nameDisplayMode')
}),
archive: new Archiving({
show: self.getStyle('showArchiveStatus') && self.getStyle('showArchiveStatus') !== 'off',
archiving: false
})
};
if (!(_properties.audioSource === null || _properties.audioSource === false)) {
widgets.muteButton = new MuteButton({
muted: _properties.publishAudio === false,
mode: chromeButtonMode.call(self, self.getStyle('buttonDisplayMode'))
});
}
var audioLevelTransformer = new AudioLevelTransformer();
var audioLevelUpdatedHandler = function(evt) {
_audioLevelMeter.setValue(audioLevelTransformer.transform(evt.audioLevel));
};
_audioLevelMeter = new AudioLevelMeter({
mode: self.getStyle('audioLevelDisplayMode')
});
_audioLevelMeter.watchVisibilityChanged(function(visible) {
if (visible) {
self.on('audioLevelUpdated', audioLevelUpdatedHandler);
} else {
self.off('audioLevelUpdated', audioLevelUpdatedHandler);
}
});
_audioLevelMeter.audioOnly(!_properties.publishVideo && _properties.publishAudio);
widgets.audioLevel = _audioLevelMeter;
if (_widgetView && _widgetView.domElement) {
_chrome = new Chrome({
parent: _widgetView.domElement
}).set(widgets).on({
muted: self.publishAudio.bind(self, false),
unmuted: self.publishAudio.bind(self, true)
});
}
};
var reset = function() {
if (_chrome) {
_chrome.destroy();
_chrome = null;
}
self.disconnect();
_microphone = null;
if (_videoElementFacade) {
_videoElementFacade.destroy();
_videoElementFacade = null;
}
cleanupLocalStream();
if (_widgetView) {
_widgetView.destroy();
_widgetView = null;
}
if (_session) {
self._.unpublishFromSession(_session, 'reset');
}
self.id = null;
self.stream = _stream = null;
_loaded = false;
self.session = _session = null;
if (!_state.isDestroyed()) { _state.set('NotPublishing'); }
};
StylableComponent(self, {
showArchiveStatus: true,
nameDisplayMode: 'auto',
buttonDisplayMode: 'auto',
audioLevelDisplayMode: _isScreenSharing ? 'off' : 'auto',
archiveStatusDisplayMode: 'auto',
backgroundImageURI: null
}, _properties.showControls, function(payload) {
logAnalyticsEvent('SetStyle', 'Publisher', payload, null, 0.1);
});
var setAudioOnly = function(audioOnly) {
if (_widgetView) {
_widgetView.audioOnly(audioOnly);
_widgetView.showPoster(audioOnly);
}
if (_audioLevelMeter) {
_audioLevelMeter.audioOnly(audioOnly);
}
};
this.publish = function(targetElement) {
logging.debug('OT.Publisher: publish');
if (_state.isAttemptingToPublish() || _state.isPublishing()) {
reset();
}
_state.set('GetUserMedia');
var audioDeviceId, videoDeviceId;
if (!_properties.constraints) {
_properties.constraints = OTHelpers.clone(defaultConstraints);
// detect which version of constraints scheme we should be using
var usingRangeBasedConstraints = global.navigator.mediaDevices
&& global.navigator.mediaDevices.getUserMedia && !_isScreenSharing;
if (_properties.audioSource === null || _properties.audioSource === false) {
_properties.constraints.audio = false;
_properties.publishAudio = false;
} else {
if (_isScreenSharing) {
if (typeof _properties.audioSource !== 'undefined') {
logging.warn('Invalid audioSource passed to Publisher - when using screen sharing no ' +
'audioSource may be used');
}
_properties.constraints.audio = false;
} else if (typeof _properties.audioSource === 'object' &&
_properties.audioSource.deviceId == null) {
logging.warn('Invalid audioSource passed to Publisher. Expected either a device ' +
'ID or device.');
} else if (_properties.audioSource) {
// picking the device id configuration
audioDeviceId = _properties.audioSource.deviceId != null
? _properties.audioSource.deviceId : _properties.audioSource;
}
if (audioDeviceId) {
if (typeof _properties.constraints.audio !== 'object') {
_properties.constraints.audio = {};
}
// constraints extended defaults
if (usingRangeBasedConstraints) {
_properties.constraints.audio.deviceId = {};
} else {
if (!_properties.constraints.audio.mandatory) {
_properties.constraints.audio.mandatory = {};
}
if (!_properties.constraints.audio.optional) {
_properties.constraints.audio.optional = [];
}
}
// setting constraints actual values
if (usingRangeBasedConstraints) {
_properties.constraints.audio.deviceId.exact = audioDeviceId;
} else {
_properties.constraints.audio.mandatory.sourceId = audioDeviceId;
}
}
}
if (_properties.videoSource === null || _properties.videoSource === false) {
_properties.constraints.video = false;
_properties.publishVideo = false;
} else {
if (typeof _properties.videoSource === 'object' &&
_properties.videoSource.deviceId == null) {
logging.warn('Invalid videoSource passed to Publisher. Expected either a device ' +
'ID or device.');
} else if (_properties.videoSource) {
videoDeviceId = _properties.videoSource.deviceId != null
? _properties.videoSource.deviceId : _properties.videoSource;
}
var _setupVideoDefaults = function() {
if (typeof _properties.constraints.video !== 'object') {
_properties.constraints.video = {};
}
if (usingRangeBasedConstraints) {
_properties.constraints.video.width = {};
_properties.constraints.video.height = {};
_properties.constraints.video.frameRate = {};
_properties.constraints.video.deviceId = {};
} else {
if (!_properties.constraints.video.mandatory) {
_properties.constraints.video.mandatory = {};
}
if (!_properties.constraints.video.optional) {
_properties.constraints.video.optional = [];
}
}
};
if (videoDeviceId) {
// _isScreenSharing is handled by the extension helpers
if (!_isScreenSharing) {
_setupVideoDefaults();
if (usingRangeBasedConstraints) {
// the mediaDevices spec uses constraints based on ideal/exact value
_properties.constraints.video.deviceId.exact = videoDeviceId;
} else {
_properties.constraints.video.mandatory.sourceId = videoDeviceId;
}
}
}
if (_properties.resolution) {
if (!_validResolutions.hasOwnProperty(_properties.resolution)) {
logging.warn('Invalid resolution passed to the Publisher. Got: ' +
_properties.resolution + ' expecting one of "' +
Object.keys(_validResolutions).join('","') + '"');
} else {
_properties.videoDimensions = _validResolutions[_properties.resolution];
_setupVideoDefaults();
if (usingRangeBasedConstraints) {
_properties.constraints.video.width.ideal =
_properties.videoDimensions.width;
_properties.constraints.video.height.ideal =
_properties.videoDimensions.height;
} else {
_properties.constraints.video.optional =
_properties.constraints.video.optional.concat([
{ minWidth: _properties.videoDimensions.width },
{ maxWidth: _properties.videoDimensions.width },
{ minHeight: _properties.videoDimensions.height },
{ maxHeight: _properties.videoDimensions.height }
]);
}
}
}
if (_properties.maxResolution) {
_setupVideoDefaults();
if (_properties.maxResolution.width > 1920) {
logging.warn(
'Invalid maxResolution passed to the Publisher. maxResolution.width must ' +
'be less than or equal to 1920'
);
_properties.maxResolution.width = 1920;
}
if (_properties.maxResolution.height > 1920) {
logging.warn(
'Invalid maxResolution passed to the Publisher. maxResolution.height must ' +
'be less than or equal to 1920'
);
_properties.maxResolution.height = 1920;
}
_properties.videoDimensions = _properties.maxResolution;
_setupVideoDefaults();
if (usingRangeBasedConstraints) {
_properties.constraints.video.width.max =
_properties.videoDimensions.width;
_properties.constraints.video.height.max =
_properties.videoDimensions.height;
} else {
_properties.constraints.video.mandatory.maxWidth =
_properties.videoDimensions.width;
_properties.constraints.video.mandatory.maxHeight =
_properties.videoDimensions.height;
}
}
if (_properties.frameRate !== void 0 &&
_validFrameRates.indexOf(_properties.frameRate) === -1) {
logging.warn('Invalid frameRate passed to the publisher got: ' +
_properties.frameRate + ' expecting one of ' + _validFrameRates.join(','));
delete _properties.frameRate;
} else if (_properties.frameRate) {
_setupVideoDefaults();
if (usingRangeBasedConstraints) {
_properties.constraints.video.frameRate.ideal = _properties.frameRate;
} else {
_properties.constraints.video.optional =
_properties.constraints.video.optional.concat([
{ minFrameRate: _properties.frameRate },
{ maxFrameRate: _properties.frameRate }
]);
}
}
}
} else {
logging.warn('You have passed your own constraints not using ours');
}
if (_properties.style) {
self.setStyle(_properties.style, null, true);
}
if (_properties.name) {
_properties.name = _properties.name.toString();
}
_properties.classNames = 'OT_root OT_publisher';
// Defer actually creating the publisher DOM nodes until we know
// the DOM is actually loaded.
EnvironmentLoader.onLoad(function() {
_widgetView = new Publisher.WidgetView(targetElement, _properties);
self.id = _widgetView.domId();
self.element = _widgetView.domElement;
_widgetView.on('videoDimensionsChanged', function(oldValue, newValue) {
if (_stream) {
_stream.setVideoDimensions(newValue.width, newValue.height);
}
self.dispatchEvent(
new Events.VideoDimensionsChangedEvent(self, oldValue, newValue)
);
});
_widgetView.on('mediaStopped', function() {
var event = new Events.MediaStoppedEvent(self);
self.dispatchEvent(event, function() {
if (!event.isDefaultPrevented()) {
if (_session) {
self._.unpublishFromSession(_session, 'mediaStopped');
} else {
self.destroy('mediaStopped');
}
}
});
});
_widgetView.on('videoElementCreated', function(element) {
var event = new Events.VideoElementCreatedEvent(element);
self.dispatchEvent(event);
});
OTHelpers.waterfall([
function(cb) {
if (_isScreenSharing) {
screenSharing.checkCapability(function(response) {
if (!response.supported) {
onScreenSharingError(
new Error('Screen Sharing is not supported in this browser')
);
} else if (response.extensionRegistered === false) {
onScreenSharingError(
new Error('Screen Sharing support in this browser requires an extension, but ' +
'one has not been registered.')
);
} else if (response.extensionRequired &&
response.extensionInstalled === false) {
onScreenSharingError(
new Error('Screen Sharing support in this browser requires an extension, but ' +
'the extension is not installed.')
);
} else {
var helper = screenSharing.pickHelper();
if (helper.proto.getConstraintsShowsPermissionUI) {
onAccessDialogOpened();
}
helper.instance.getConstraints(options.videoSource, _properties.constraints,
function(err, constraints) {
if (helper.proto.getConstraintsShowsPermissionUI) {
onAccessDialogClosed();
}
if (err) {
if (err.message === 'PermissionDeniedError') {
onAccessDenied(err);
} else {
onScreenSharingError(err);
}
} else {
_properties.constraints = constraints;
cb();
}
});
}
});
} else {
deviceHelpers.shouldAskForDevices(function(devices) {
if (!devices.video) {
logging.warn('Setting video constraint to false, there are no video sources');
_properties.constraints.video = false;
}
if (!devices.audio) {
logging.warn('Setting audio constraint to false, there are no audio sources');
_properties.constraints.audio = false;
}
_videoDevices = devices.videoDevices;
_audioDevices = devices.audioDevices;
cb();
});
}
},
function() {
if (_state.isDestroyed()) {
return;
}
Publisher.getUserMedia(
_properties.constraints,
onStreamAvailable,
onStreamAvailableError,
onAccessDialogOpened,
onAccessDialogClosed,
onAccessDenied
);
}
]);
}, self);
return self;
};
/**
* Starts publishing audio (if it is currently not being published)
* when the value is true; stops publishing audio
* (if it is currently being published) when the value is false.
*
* @param {Boolean} value Whether to start publishing audio (true)
* or not (false).
*
* @see OT.initPublisher()
* @see Stream.hasAudio
* @see StreamPropertyChangedEvent
* @method #publishAudio
* @memberOf Publisher
*/
this.publishAudio = function(value) {
_properties.publishAudio = value;
if (_microphone) {
_microphone.muted(!value);
}
if (_chrome) {
_chrome.muteButton.muted(!value);
}
if (_session && _stream) {
_stream.setChannelActiveState('audio', value);
}
if (_audioLevelMeter) {
_audioLevelMeter.audioOnly(!_properties.publishVideo && _properties.publishAudio);
}
return self;
};
/**
* Starts publishing video (if it is currently not being published)
* when the value is true; stops publishing video
* (if it is currently being published) when the value is false.
*
* @param {Boolean} value Whether to start publishing video (true)
* or not (false).
*
* @see OT.initPublisher()
* @see Stream.hasVideo
* @see StreamPropertyChangedEvent
* @method #publishVideo
* @memberOf Publisher
*/
this.publishVideo = function(value) {
var oldValue = _properties.publishVideo;
_properties.publishVideo = value;
if (_session && _stream && _properties.publishVideo !== oldValue) {
_stream.setChannelActiveState('video', value);
}
// We currently do this event if the value of publishVideo has not changed
// This is because the state of the video tracks enabled flag may not match
// the value of publishVideo at this point. This will be tidied up shortly.
if (_webRTCStream) {
var videoTracks = _webRTCStream.getVideoTracks();
for (var i = 0, num = videoTracks.length; i < num; ++i) {
videoTracks[i].enabled = value;
}
}
setAudioOnly(!value);
return self;
};
/**
* Deletes the Publisher object and removes it from the HTML DOM.
*
* The Publisher object dispatches a destroyed event when the DOM
* element is removed.
*
You can use the string as the value for a data URL scheme passed to the src parameter of * an image file, as in the following:
* *
* var imgData = publisher.getImgData();
*
* var img = document.createElement("img");
* img.setAttribute("src", "data:image/png;base64," + imgData);
* var imgWin = window.open("about:blank", "Screenshot");
* imgWin.document.write("<body></body>");
* imgWin.document.body.appendChild(img);
*
*
* @method #getImgData
* @memberOf Publisher
* @return {String} The base-64 encoded string. Returns an empty string if there is no video.
*/
this.getImgData = function() {
if (!_loaded) {
logging.error(
'OT.Publisher.getImgData: Cannot getImgData before the Publisher is publishing.'
);
return null;
}
return _videoElementFacade.imgData();
};
// API Compatibility layer for Flash Publisher, this could do with some tidyup.
this._ = {
publishToSession: function(session) {
// Add session property to Publisher
self.session = _session = session;
_streamId = uuid();
var createStream = function() {
// Bail if this.session is gone, it means we were unpublished
// before createStream could finish.
if (!_session) { return; }
// make sure we trigger an error if we are not getting any "ack" after a reasonable
// amount of time
var publishGuardingTo = setTimeout(function() {
onPublishingTimeout(session);
}, PUBLISH_MAX_DELAY);
self.on('publishComplete', function() {
clearTimeout(publishGuardingTo);
});
_state.set('PublishingToSession');
var onStreamRegistered = function(err, streamId, message) {
if (err) {
// @todo we should respect err.code here and translate it to the local
// client equivalent.
var errorCode, errorMessage;
var knownErrorCodes = [403, 404, 409];
if (err.code && knownErrorCodes.indexOf(err.code) > -1) {
errorCode = ExceptionCodes.UNABLE_TO_PUBLISH;
errorMessage = err.message;
} else {
errorCode = ExceptionCodes.UNEXPECTED_SERVER_RESPONSE;
errorMessage = 'Unexpected server response. Try this operation again later.';
}
var options = {
failureReason: 'Publish',
failureCode: errorCode,
failureMessage: errorMessage
};
logConnectivityEvent('Failure', null, options);
if (_state.isAttemptingToPublish()) {
self.trigger('publishComplete', new OTError(errorCode, errorMessage));
}
OTError.handleJsException(
err.message,
errorCode,
{
session: _session,
target: self
}
);
return;
}
self.streamId = _streamId = streamId;
_iceServers = parseIceServers(message);
};
var streamDimensions = getVideoDimensions();
var streamChannels = [];
if (!(_properties.videoSource === null || _properties.videoSource === false)) {
streamChannels.push(new StreamChannel({
id: 'video1',
type: 'video',
active: _properties.publishVideo,
orientation: VideoOrientation.ROTATED_NORMAL,
frameRate: _properties.frameRate,
width: streamDimensions.width,
height: streamDimensions.height,
source: _isScreenSharing ? 'screen' : 'camera',
fitMode: _properties.fitMode
}));
}
if (!(_properties.audioSource === null || _properties.audioSource === false)) {
streamChannels.push(new StreamChannel({
id: 'audio1',
type: 'audio',
active: _properties.publishAudio
}));
}
session._.streamCreate(_properties.name || '', _streamId,
_properties.audioFallbackEnabled, streamChannels, onStreamRegistered);
};
if (_loaded) {
createStream.call(self);
} else {
self.on('initSuccess', createStream, self);
}
logConnectivityEvent('Attempt', {
dataChannels: _properties.channels
});
return self;
},
unpublishFromSession: function(session, reason) {
if (!_session || session.id !== _session.id) {
logging.warn('The publisher ' + _guid + ' is trying to unpublish from a session ' +
session.id + ' it is not attached to (it is attached to ' +
(_session && _session.id || 'no session') + ')');
return self;
}
if (session.isConnected() && self.stream) {
session._.streamDestroy(self.stream.id);
}
// Disconnect immediately, rather than wait for the WebSocket to
// reply to our destroyStream message.
self.disconnect();
if (_state.isAttemptingToPublish()) {
logConnectivityEvent('Cancel', { reason: 'unpublish' });
}
self.session = _session = null;
// We're back to being a stand-alone publisher again.
if (!_state.isDestroyed()) { _state.set('MediaBound'); }
if (_connectivityAttemptPinger) {
_connectivityAttemptPinger.stop();
_connectivityAttemptPinger = null;
}
logAnalyticsEvent('unpublish', 'Success', { sessionId: session.id });
self._.streamDestroyed(reason);
return self;
},
streamDestroyed: function(reason) {
if (['reset'].indexOf(reason) < 0) {
var event = new Events.StreamEvent('streamDestroyed', _stream, reason, true);
var defaultAction = function() {
if (!event.isDefaultPrevented()) {
self.destroy();
}
};
self.dispatchEvent(event, defaultAction);
}
},
archivingStatus: function(status) {
if (_chrome) {
_chrome.archive.setArchiving(status);
}
return status;
},
webRtcStream: function() {
return _webRTCStream;
},
switchTracks: function() {
return new Bluebird.Promise(function(resolve, reject) {
Publisher.getUserMedia(
_properties.constraints,
function(newStream) {
cleanupLocalStream();
_webRTCStream = newStream;
_microphone = new Publisher.Microphone(_webRTCStream, !_properties.publishAudio);
var videoContainerOptions = {
muted: true,
error: onVideoError
};
_videoElementFacade = _widgetView.bindVideo(_webRTCStream, videoContainerOptions,
function(err) {
if (err) {
onLoadFailure(err);
reject(err);
}
});
var replacePromises = [];
Object.keys(_peerConnections).forEach(function(connectionId) {
var peerConnection = _peerConnections[connectionId];
peerConnection.getSenders().forEach(function(sender) {
if (sender.track.kind === 'audio' && newStream.getAudioTracks().length) {
replacePromises.push(sender.replaceTrack(newStream.getAudioTracks()[0]));
} else if (sender.track.kind === 'video' && newStream.getVideoTracks().length) {
replacePromises.push(sender.replaceTrack(newStream.getVideoTracks()[0]));
}
});
});
Bluebird.all(replacePromises).then(resolve, reject);
},
function(error) {
onStreamAvailableError(error);
reject(error);
},
onAccessDialogOpened,
onAccessDialogClosed,
function(error) {
onAccessDenied(error);
reject(error);
});
});
},
/**
* @param {string=} windowId
*/
switchAcquiredWindow: function(windowId) {
if (OTHelpers.env.name !== 'Firefox' || OTHelpers.env.version < 38) {
throw new Error('switchAcquiredWindow is an experimental method and is not supported by' +
'the current platform');
}
if (typeof windowId !== 'undefined') {
if (typeof _properties.constraints.video === 'boolean') {
// video could just be true or false, where true means "yes I want
// video" without configuring any other details.
if (_properties.constraints.video === false) {
// This doesn't even make sense as video can't be disabled if this
// window acquisition thing was to work. Probably developer error...
throw new Error('Cannot switchAcquiredWindow when there is no video');
}
// video === true and video === {} actually mean the same thing. We'll
// change it to {} here so that we can safely add a `browserWindow`
// property below.
_properties.constraints.video = {};
}
_properties.constraints.video.browserWindow = windowId;
}
logAnalyticsEvent('SwitchAcquiredWindow', 'Attempt', {
constraints: _properties.constraints
});
var switchTracksPromise = self._.switchTracks();
// "listening" promise completion just for analytics
switchTracksPromise.then(function() {
logAnalyticsEvent('SwitchAcquiredWindow', 'Success', {
constraints: _properties.constraints
});
}, function(error) {
logAnalyticsEvent('SwitchAcquiredWindow', 'Failure', {
error: error,
constraints: _properties.constraints
});
});
return switchTracksPromise;
},
getDataChannel: function(label, options, completion) {
var pc = _peerConnections[Object.keys(_peerConnections)[0]];
// @fixme this will fail if it's called before we have a PublisherPeerConnection.
// I.e. before we have a subscriber.
if (!pc) {
completion(new OTHelpers.Error(
'Cannot create a DataChannel before there is a subscriber.'
));
return;
}
pc.getDataChannel(label, options, completion);
},
iceRestart: function(force) {
var peerConnection, fromConnectionId;
for (fromConnectionId in _peerConnections) {
if (_peerConnections.hasOwnProperty(fromConnectionId)) {
peerConnection = _peerConnections[fromConnectionId];
if (force || !peerConnection.iceConnectionStateIsConnected()) {
logRepublish('Attempt', { remoteConnectionId: fromConnectionId });
_peerConnections[fromConnectionId].createOfferWithIceRestart();
} else {
logging.debug('Publisher: Skipping ice restart for ' + fromConnectionId +
', we are connected.');
}
}
}
}
};
this.detectDevices = function() {
logging.warn('Fixme: Haven\'t implemented detectDevices');
};
this.detectMicActivity = function() {
logging.warn('Fixme: Haven\'t implemented detectMicActivity');
};
this.getEchoCancellationMode = function() {
logging.warn('Fixme: Haven\'t implemented getEchoCancellationMode');
return 'fullDuplex';
};
this.setMicrophoneGain = function() {
logging.warn('Fixme: Haven\'t implemented setMicrophoneGain');
};
this.getMicrophoneGain = function() {
logging.warn('Fixme: Haven\'t implemented getMicrophoneGain');
return 0.5;
};
this.setCamera = function() {
logging.warn('Fixme: Haven\'t implemented setCamera');
};
this.setMicrophone = function() {
logging.warn('Fixme: Haven\'t implemented setMicrophone');
};
// Platform methods:
this.guid = function() {
return _guid;
};
this.videoElement = function() {
return _videoElementFacade.domElement();
};
this.setStream = assignStream;
this.isWebRTC = true;
this.isLoading = function() {
return _widgetView && _widgetView.loading();
};
/**
* Returns the width, in pixels, of the Publisher video. This may differ from the
* resolution property passed in as the properties property
* the options passed into the OT.initPublisher() method, if the browser
* does not support the requested resolution.
*
* @method #videoWidth
* @memberOf Publisher
* @return {Number} the width, in pixels, of the Publisher video.
*/
this.videoWidth = function() {
return _videoElementFacade.videoWidth();
};
/**
* Returns the height, in pixels, of the Publisher video. This may differ from the
* resolution property passed in as the properties property
* the options passed into the OT.initPublisher() method, if the browser
* does not support the requested resolution.
*
* @method #videoHeight
* @memberOf Publisher
* @return {Number} the height, in pixels, of the Publisher video.
*/
this.videoHeight = function() {
return _videoElementFacade.videoHeight();
};
// Make read-only: element, guid, _.webRtcStream
this.on('styleValueChanged', updateChromeForStyleChange, this);
_state = new PublishingState(stateChangeFailed);
this.accessAllowed = false;
/**
* Dispatched when the user has clicked the Allow button, granting the
* app access to the camera and microphone. The Publisher object has an
* accessAllowed property which indicates whether the user
* has granted access to the camera and microphone.
* @see Event
* @name accessAllowed
* @event
* @memberof Publisher
*/
/**
* Dispatched when the user has clicked the Deny button, preventing the
* app from having access to the camera and microphone.
* @see Event
* @name accessDenied
* @event
* @memberof Publisher
*/
/**
* Dispatched when the Allow/Deny dialog box is opened. (This is the dialog box in which
* the user can grant the app access to the camera and microphone.)
* @see Event
* @name accessDialogOpened
* @event
* @memberof Publisher
*/
/**
* Dispatched when the Allow/Deny box is closed. (This is the dialog box in which the
* user can grant the app access to the camera and microphone.)
* @see Event
* @name accessDialogClosed
* @event
* @memberof Publisher
*/
/**
* Dispatched periodically to indicate the publisher's audio level. The event is dispatched
* up to 60 times per second, depending on the browser. The audioLevel property
* of the event is audio level, from 0 to 1.0. See {@link AudioLevelUpdatedEvent} for more
* information.
* * The following example adjusts the value of a meter element that shows volume of the * publisher. Note that the audio level is adjusted logarithmically and a moving average * is applied: *
*
* var movingAvg = null;
* publisher.on('audioLevelUpdated', function(event) {
* if (movingAvg === null || movingAvg <= event.audioLevel) {
* movingAvg = event.audioLevel;
* } else {
* movingAvg = 0.7 * movingAvg + 0.3 * event.audioLevel;
* }
*
* // 1.5 scaling to map the -30 - 0 dBm range to [0,1]
* var logLevel = (Math.log(movingAvg) / Math.LN10) / 1.5 + 1;
* logLevel = Math.min(Math.max(logLevel, 0), 1);
* document.getElementById('publisherMeter').value = logLevel;
* });
*
* This example shows the algorithm used by the default audio level indicator displayed
* in an audio-only Publisher.
*
* @name audioLevelUpdated
* @event
* @memberof Publisher
* @see AudioLevelUpdatedEvent
*/
/**
* The publisher has started streaming to the session.
* @name streamCreated
* @event
* @memberof Publisher
* @see StreamEvent
* @see Session.publish()
*/
/**
* The publisher has stopped streaming to the session. The default behavior is that
* the Publisher object is removed from the HTML DOM. The Publisher object dispatches a
* destroyed event when the element is removed from the HTML DOM. If you call the
* preventDefault() method of the event object in the event listener, the default
* behavior is prevented, and you can, optionally, retain the Publisher for reuse or clean it up
* using your own code.
* @name streamDestroyed
* @event
* @memberof Publisher
* @see StreamEvent
*/
/**
* Dispatched when the Publisher element is removed from the HTML DOM. When this event
* is dispatched, you may choose to adjust or remove HTML DOM elements related to the publisher.
* @name destroyed
* @event
* @memberof Publisher
*/
/**
* Dispatched when the video dimensions of the video change. This can only occur in when the
* stream.videoType property is set to "screen" (for a screen-sharing
* video stream), when the user resizes the window being captured. This event object has a
* newValue property and an oldValue property, representing the new and
* old dimensions of the video. Each of these has a height property and a
* width property, representing the height and width, in pixels.
* @name videoDimensionsChanged
* @event
* @memberof Publisher
* @see VideoDimensionsChangedEvent
*/
/**
* Dispatched when the Publisher's video element is created. Add a listener for this event when
* you set the insertDefaultUI option to false in the call to the
* OT.initPublisher() method. The element
* property of the event object is a reference to the Publisher's video element
* (or in Internet Explorer the object element containing the video). Add it to
* the HTML DOM to display the video. When you set the insertDefaultUI option to
* false, the video (or object) element is not automatically
* inserted into the DOM.
*
* Add a listener for this event only if you have set the insertDefaultUI option to
* false. If you have not set insertDefaultUI option to
* false, do not move the video (or object) element in
* in the HTML DOM. Doing so causes the Publisher object to be destroyed.
*
* @name videoElementCreated
* @event
* @memberof Publisher
* @see VideoElementCreatedEvent
*/
/**
* The user has stopped screen-sharing for the published stream. This event is only dispatched
* for screen-sharing video streams.
* @name mediaStopped
* @event
* @memberof Publisher
* @see StreamEvent
*/
};
// Helper function to generate unique publisher ids
Publisher.nextId = uuid;
Publisher.audioContext = audioContext;
Publisher.getUserMedia = getUserMedia;
Publisher.Microphone = Microphone;
Publisher.PublisherPeerConnection = PublisherPeerConnection;
Publisher.WidgetView = WidgetView;
module.exports = Publisher;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"../../helpers/audio_context.js":141,"../../helpers/connectivity_attempt_pinger.js":145,"../../helpers/device_helpers.js":148,"../../helpers/get_user_media.js":150,"../../helpers/properties.js":154,"../../helpers/video_orientation.js":163,"../../helpers/widget_view.js":164,"../analytics.js":166,"../api_key.js":168,"../audio_level_transformer":170,"../chrome/archiving.js":172,"../chrome/audio_level_meter.js":173,"../chrome/backing_bar.js":174,"../chrome/chrome.js":176,"../chrome/mute_button.js":177,"../chrome/name_panel.js":178,"../environment_loader.js":181,"../events.js":182,"../exception_codes.js":183,"../interval_runner.js":186,"../logging.js":187,"../messaging/raptor/parse_ice_servers.js":193,"../ot_error.js":208,"../peer_connection/publisher_peer_connection.js":218,"../screensharing/screen_sharing.js":235,"../stream_channel.js":242,"../styling/stylable_component.js":244,"../system_requirements.js":249,"./max_delay.js":226,"./microphone.js":227,"./state.js":228,"@opentok/ot-helpers":4,"bluebird":70,"lodash.pick":121,"uuid":137}],225:[function(require,module,exports){
'use strict';
var logging = require('../logging.js');
var OTHelpers = require('@opentok/ot-helpers');
var Publisher = require('./index.js');
var sessionObjects = require('../session/objects.js');
var ExceptionCodes = require('../exception_codes.js');
var OTError = require('../ot_error.js');
/**
*
* Initializes and returns a Publisher object. You can then pass this Publisher
* object to Session.publish() to publish a stream to a session.
*
* Note: If you intend to reuse a Publisher object created using
* OT.initPublisher() to publish to different sessions sequentially,
* call either Session.disconnect() or Session.unpublish().
* Do not call both. Then call the preventDefault() method of the
* streamDestroyed or sessionDisconnected event object to prevent the
* Publisher object from being removed from the page.
*
id attribute of the
* existing DOM element used to determine the location of the Publisher video in the HTML DOM. See
* the insertMode property of the properties parameter. If you do not
* specify a targetElement, the application appends a new DOM element to the HTML
* body.
*
*
* The application throws an error if an element with an ID set to the
* targetElement value does not exist in the HTML DOM.
*
true) or not (false). The audio-fallback
* feature is available in sessions that use the the OpenTok Media Router. With the audio-fallback
* feature enabled (the default), when the server determines that a stream's quality has degraded
* significantly for a specific subscriber, it disables the video in that subscriber in order to
* preserve audio quality. For streams that use a camera as a video source, the default setting is
* true (the audio-fallback feature is enabled). The default setting is
* false (the audio-fallback feature is disabled) for screen-sharing streams, which
* have the videoSource property set to "application",
* "screen", or "window" in the OT.initPublisher()
* options. For more information, see the Subscriber
* videoDisabled event and
* the OpenTok Media
* Router and media modes.
* OT.initPublisher() fails with an
* error (error code 1500, "Unable to Publish") passed to the completion handler function.
*
* If you set this property to null or false, the browser does not
* request access to the microphone, and no audio is published.
*
*
"cover" — The video is cropped if its dimensions do not match those of
* the DOM element. This is the default setting for videos publishing a camera feed.
* "contain" — The video is letterboxed if its dimensions do not match
* those of the DOM element. This is the default setting for screen-sharing videos.
* If the publisher specifies a frame rate, the actual frame rate of the video stream
* is set as the frameRate property of the Stream object, though the actual frame rate
* will vary based on changing network and system conditions. If the developer does not specify a
* frame rate, this property is undefined.
*
* For sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed, lowering the frame rate or lowering the resolution reduces * the maximum bandwidth the stream can use. However, in sessions with the media mode set to * relayed, lowering the frame rate or resolution may not reduce the stream's bandwidth. *
*
* You can also restrict the frame rate of a Subscriber's video stream. To restrict the frame rate
* a Subscriber, call the restrictFrameRate() method of the subscriber, passing in
* true.
* (See Subscriber.restrictFrameRate().)
*
element property of the Publisher object) or (if the height is specified as
* a percentage) its parent DOM element (see
* Resizing
* or repositioning a video).
* true, the default) or not (false). The default UI element contains
* user interface controls, a video loading indicator, and automatic video cropping or
* letterboxing, in addition to the video. (If you leave insertDefaultUI set to
* true, you can control individual UI settings using the fitMode,
* showControls, and style options.)
*
* If you set this option to false, OpenTok.js does not insert a default UI element
* in the HTML DOM, and the element property of the Publisher object is undefined.
* Instead, the Publisher object dispatches a
* videoElementCreated event when
* the video element (or in Internet Explorer the object element
* containing the video) is created. The element property of the event object is a
* reference to the Publisher's video (or object) element. Add it to
* the HTML DOM to display the video.
*
* Set this option to false if you want to move the Publisher's video
* (or object) element in the HTML DOM.
*
* If you set this to false, do not set the targetElement parameter.
* (This results in an error passed into to the OT.initPublisher() callback
* function.) To add the video to the HTML DOM, add an event listener for the
* videoElementCreated event, and then add the element property of
* the event object into the HTML DOM.
*
targetElement parameter. This string can
* have the following values:
* *
"replace" The Publisher object replaces contents of the
* targetElement. This is the default."after" The Publisher object is a new element inserted after
* the targetElement in the HTML DOM. (Both the Publisher and targetElement have the
* same parent element.)"before" The Publisher object is a new element inserted before
* the targetElement in the HTML DOM. (Both the Publisher and targetElement have the same
* parent element.)"append" The Publisher object is a new element added as a child
* of the targetElement. If there are other child elements, the Publisher is appended as
* the last child element of the targetElement. Do not move the publisher element or its parent elements in the DOM
* heirarchy. Use CSS to resize or reposition the publisher video's element
* (the element property of the Publisher object) or its parent element (see
* Resizing
* or repositioning a video.
videoSource property is set to
* "application", "screen", or "window"
* (when the publisher is screen-sharing). The resolution of the
* stream will match the captured screen region unless the region is greater than the
* maxResolution setting. Set this to an object that has two properties:
* width and height (both numbers). The maximum value for each of
* the width and height properties is 1920, and the minimum value
* is 10.
* true
* (the video image is mirrored), except for a screen-sharing video (when the
* videoSource property is set to "application",
* "screen", or "window"
* (in which case the default is false). This property
* does not affect the display on subscribers' views of the video.
* true). This setting applies when you pass
* the Publisher object in a call to the Session.publish() method.
* true). This setting applies when you pass
* the Publisher object in a call to the Session.publish() method.
* "widthxheight", where the width and height are represented in
* pixels. Valid values are "1280x720", "640x480", and
* "320x240". The published video will only use the desired resolution if the
* client configuration supports it. Some browsers and clients do not support each of these
* resolution settings.
*
* The requested resolution of a video stream is set as the videoDimensions.width and
* videoDimensions.height properties of the Stream object.
*
* The default resolution for a stream (if you do not specify a resolution) is 640x480 pixels. * If the client system cannot support the resolution you requested, the the stream will use the * next largest setting supported. *
*
* The actual resolution used by the Publisher is returned by the videoHeight() and
* videoWidth() methods of the Publisher object. The actual resolution of a
* Subscriber video stream is returned by the videoHeight() and
* videoWidth() properties of the Subscriber object. These may differ from the values
* of the resolution property passed in as the properties property of the
* OT.initPublisher() method, if the browser does not support the requested
* resolution.
*
* For sessions that use the OpenTok Media Router (sessions with the * media mode * set to routed, lowering the frame rate or lowering the resolution reduces the maximum bandwidth * the stream can use. However, in sessions that have the media mode set to relayed, lowering the * frame rate or resolution may not reduce the stream's bandwidth. *
*true) for the Publisher. These controls include the name
* display, the audio level indicator, and the microphone control button. You can turn off all user
* interface controls by setting this property to false. You can control the display
* of individual user interface controls by leaving this property set to true (the
* default) and setting individual properties of the style property.
* style object includes
* the following properties:
* audioLevelDisplayMode (String) — How to display the audio level
* indicator. Possible values are: "auto" (the indicator is displayed when the
* video is disabled), "off" (the indicator is not displayed), and
* "on" (the indicator is always displayed).archiveStatusDisplayMode (String) — How to display the archive status
* indicator. Possible values are: "auto" (the indicator is displayed when the
* session is being recorded), "off" (the indicator is not displayed). If you
* disable the archive status display indicator, you can display your own user interface
* notifications based on the archiveStarted and archiveStopped
* events dispatched by the Session object.backgroundImageURI (String) — A URI for an image to display as
* the background image when a video is not displayed. (A video may not be displayed if
* you call publishVideo(false) on the Publisher object). You can pass an http
* or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the
* data URI scheme (instead of http or https) and pass in base-64-encrypted
* PNG data, such as that obtained from the
* Publisher.getImgData() method. For example,
* you could set the property to "data:VBORw0KGgoAA...", where the portion of the
* string after "data:" is the result of a call to
* Publisher.getImgData(). If the URL or the image data is invalid, the property
* is ignored (the attempt to set the image fails silently).buttonDisplayMode (String) — How to display the microphone controls
* Possible values are: "auto" (controls are displayed when the stream is first
* displayed and when the user mouses over the display), "off" (controls are not
* displayed), and "on" (controls are always displayed).nameDisplayMode (String) Whether to display the stream name.
* Possible values are: "auto" (the name is displayed when the stream is first
* displayed and when the user mouses over the display), "off" (the name is not
* displayed), and "on" (the name is always displayed).OT.initPublisher() fails with an
* error (error code 1500, "Unable to Publish") passed to the completion handler function.
*
* If you set this property to null or false, the browser does not
* request access to the camera, and no video is published. In a voice-only call, set this
* property to null or false for each Publisher.
*
* To publish a screen-sharing stream, set this property to "application",
* "screen", or "window". Call
* OT.checkScreenSharingCapability() to check
* if screen sharing is supported. When you set the videoSource property to
* "application", "screen", or "window", the
* following are default values for other properties: audioFallbackEnabled == false,
* maxResolution == {width: 1920, height: 1920}, mirror == false,
* scaleMode == "fit". Also, the default scaleMode setting for
* subscribers to the stream is "fit".
*
element property of the Publisher object) or (if the width is specified as
* a percentage) its parent DOM element (see
* Resizing
* or repositioning a video).
* error. On success, the error object is set to null. On
* failure, the error object has two properties: code (an integer) and
* message (a string), which identify the cause of the failure. The method succeeds
* when the user grants access to the camera and microphone. The method fails if the user denies
* access to the camera and microphone. The completionHandler function is called
* before the Publisher dispatches an accessAllowed (success) event or an
* accessDenied (failure) event.
*
* The following code adds a completionHandler when calling the
* OT.initPublisher() method:
*
* var publisher = OT.initPublisher('publisher', null, function (error) {
* if (error) {
* console.log(error);
* } else {
* console.log("Publisher initialized.");
* }
* });
*
*
* @returns {Publisher} The Publisher object.
* @see Session.publish()
* @method OT.initPublisher
* @memberof OT
*/
module.exports = function initPublisher(targetElement, properties, completionHandler) {
logging.debug('OT.initPublisher(' + targetElement + ')');
// To support legacy (apikey, targetElement, properties) users
// we check to see if targetElement is actually an apikey. Which we ignore.
if (typeof targetElement === 'string' && !document.getElementById(targetElement)) {
targetElement = properties;
properties = completionHandler;
completionHandler = arguments[3];
}
if (typeof targetElement === 'function') {
completionHandler = targetElement;
properties = undefined;
targetElement = undefined;
} else if (OTHelpers.isObject(targetElement) && !(OTHelpers.isElementNode(targetElement))) {
completionHandler = properties;
properties = targetElement;
targetElement = undefined;
}
if (typeof properties === 'function') {
completionHandler = properties;
properties = undefined;
}
var errMsg;
if (properties && !OTHelpers.isObject(properties)) {
errMsg = 'properties argument to Publisher constructor, if provided, should be an object';
properties = undefined;
}
if (properties && properties.insertDefaultUI === false && targetElement) {
errMsg = 'You cannot specify a target element if insertDefaultUI is false';
}
var publisher = new Publisher(properties);
sessionObjects.publishers.add(publisher);
var triggerCallback = function triggerCallback() {
if (completionHandler && OTHelpers.isFunction(completionHandler)) {
completionHandler.apply(null, arguments);
completionHandler = undefined;
}
};
if (errMsg !== undefined) {
logging.error(errMsg);
triggerCallback(new OTError(ExceptionCodes.INVALID_PARAMETER, errMsg));
}
var removeInitSuccessAndCallComplete = function removeInitSuccessAndCallComplete(err) {
publisher.off('publishComplete', removeHandlersAndCallComplete);
triggerCallback(err);
};
var removeHandlersAndCallComplete = function removeHandlersAndCallComplete(err) {
publisher.off('initSuccess', removeInitSuccessAndCallComplete);
// We're only handling the error case here as we're just
// initing the publisher, not actually attempting to publish.
if (err) { triggerCallback(err); }
};
publisher.once('initSuccess', removeInitSuccessAndCallComplete);
publisher.once('publishComplete', removeHandlersAndCallComplete);
publisher.publish(targetElement);
return publisher;
};
},{"../exception_codes.js":183,"../logging.js":187,"../ot_error.js":208,"../session/objects.js":239,"./index.js":224,"@opentok/ot-helpers":4}],226:[function(require,module,exports){
'use strict';
module.exports = 15000;
},{}],227:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
/*
* A Publishers Microphone.
*
* TODO
* * bind to changes in mute/unmute/volume/etc and respond to them
*/
module.exports = function Microphone(webRTCStream, muted) {
var _muted;
OTHelpers.defineProperties(this, {
muted: {
get: function() {
return _muted;
},
set: function(muted) {
if (_muted === muted) { return; }
_muted = muted;
var audioTracks = webRTCStream.getAudioTracks();
for (var i = 0, num = audioTracks.length; i < num; ++i) {
audioTracks[i].enabled = !_muted;
}
}
}
});
// Set the initial value
if (muted !== undefined) {
this.muted(muted === true);
} else if (webRTCStream.getAudioTracks().length) {
this.muted(!webRTCStream.getAudioTracks()[0].enabled);
} else {
this.muted(false);
}
};
},{"@opentok/ot-helpers":4}],228:[function(require,module,exports){
'use strict';
var generateSimpleStateMachine = require('../generate_simple_state_machine.js');
// Models a Publisher's publishing State
//
// Valid States:
// NotPublishing
// GetUserMedia
// BindingMedia
// MediaBound
// PublishingToSession
// Publishing
// Failed
// Destroyed
//
//
// Valid Transitions:
// NotPublishing ->
// GetUserMedia
//
// GetUserMedia ->
// BindingMedia
// | Failed (Failure Reasons -> stream error, constraints,
// (permission denied
// | NotPublishing (destroy()
//
//
// BindingMedia ->
// MediaBound
// | Failed (Failure Reasons -> Anything to do with the media
// (being invalid, the media never plays
// | NotPublishing (destroy()
//
// MediaBound ->
// PublishingToSession (MediaBound could transition to PublishingToSession
// (if a stand-alone publish is bound to a session
// | Failed (Failure Reasons -> media issues with a stand-alone publisher
// | NotPublishing (destroy()
//
// PublishingToSession
// Publishing
// | Failed (Failure Reasons -> timeout while waiting for ack of
// (stream registered. We do not do this right now
// | NotPublishing (destroy()
//
//
// Publishing ->
// NotPublishing (Unpublish
// | Failed (Failure Reasons -> loss of network, media error, anything
// (that causes *all* Peer Connections to fail (less than all
// (failing is just an error, all is failure)
// | NotPublishing (destroy()
//
// Failed ->
// Destroyed
//
// Destroyed -> (Terminal state
//
//
var validStates = [
'NotPublishing', 'GetUserMedia', 'BindingMedia', 'MediaBound',
'PublishingToSession', 'Publishing', 'Failed',
'Destroyed'
];
var validTransitions = {
NotPublishing: ['NotPublishing', 'GetUserMedia', 'Destroyed'],
GetUserMedia: ['BindingMedia', 'Failed', 'NotPublishing', 'Destroyed'],
BindingMedia: ['MediaBound', 'Failed', 'NotPublishing', 'Destroyed'],
MediaBound: ['NotPublishing', 'PublishingToSession', 'Failed', 'Destroyed'],
PublishingToSession: ['NotPublishing', 'Publishing', 'Failed', 'Destroyed'],
Publishing: ['NotPublishing', 'MediaBound', 'Failed', 'Destroyed'],
Failed: ['Destroyed'],
Destroyed: []
};
var initialState = 'NotPublishing';
var PublishingState = generateSimpleStateMachine(initialState, validStates, validTransitions);
module.exports = PublishingState;
PublishingState.prototype.isDestroyed = function() {
return this.current === 'Destroyed';
};
PublishingState.prototype.isAttemptingToPublish = function() {
return [
'GetUserMedia',
'BindingMedia',
'MediaBound',
'PublishingToSession'
].indexOf(this.current) !== -1;
};
PublishingState.prototype.isPublishing = function() {
return this.current === 'Publishing';
};
},{"../generate_simple_state_machine.js":184}],229:[function(require,module,exports){
'use strict';
var Bluebird = require('bluebird');
module.exports = function httpTest(config) {
var _httpConfig = config.httpConfig;
function measureDownloadBandwidth(url) {
var xhr = new XMLHttpRequest();
var resultPromise = new Bluebird.Promise(function(resolve, reject) {
var startTs = Date.now();
var progressLoaded = 0;
function calculate(loaded) {
return 1000 * 8 * loaded / (Date.now() - startTs);
}
xhr.addEventListener('load', function(evt) {
resolve(calculate(evt.loaded));
});
xhr.addEventListener('abort', function() {
resolve(calculate(progressLoaded));
});
xhr.addEventListener('error', function(evt) {
reject(evt);
});
xhr.addEventListener('progress', function(evt) {
progressLoaded = evt.loaded;
});
xhr.open('GET', url + '?_' + Math.random());
xhr.send();
});
return {
promise: resultPromise,
abort: function() {
xhr.abort();
}
};
}
/**
* Measures the bandwidth in bps.
*
* @param {string} url
* @param {ArrayBuffer} payload
* @returns {{promise: Promise, abort: function}}
*/
function measureUploadBandwidth(url, payload) {
var xhr = new XMLHttpRequest();
var resultPromise = new Bluebird.Promise(function(resolve, reject) {
var startTs, lastTs, lastLoaded;
xhr.upload.addEventListener('progress', function(evt) {
if (!startTs) {
startTs = Date.now();
}
lastLoaded = evt.loaded;
});
xhr.addEventListener('load', function() {
lastTs = Date.now();
resolve(1000 * 8 * lastLoaded / (lastTs - startTs));
});
xhr.addEventListener('error', function(e) {
reject(e);
});
xhr.addEventListener('abort', function() {
reject();
});
xhr.open('POST', url);
xhr.send(payload);
});
return {
promise: resultPromise,
abort: function() {
xhr.abort();
}
};
}
function doDownload(url, maxDuration) {
var measureResult = measureDownloadBandwidth(url);
setTimeout(function() {
measureResult.abort();
}, maxDuration);
return measureResult.promise;
}
function loopUpload(url, initialSize, maxDuration) {
return new Bluebird.Promise(function(resolve) {
var lastMeasureResult;
var lastBandwidth = 0;
setTimeout(function() {
lastMeasureResult.abort();
resolve(lastBandwidth);
}, maxDuration);
function loop(loopSize) {
lastMeasureResult = measureUploadBandwidth(url, new ArrayBuffer(loopSize / 8));
lastMeasureResult.promise
.then(function(bandwidth) {
lastBandwidth = bandwidth;
loop(loopSize * 2);
});
}
loop(initialSize);
});
}
return Bluebird
.all([
doDownload(_httpConfig.downloadUrl, _httpConfig.duration * 1000),
loopUpload(_httpConfig.uploadUrl, _httpConfig.uploadSize, _httpConfig.duration * 1000)
])
.then(function(results) {
return {
downloadBandwidth: results[0],
uploadBandwidth: results[1]
};
});
};
},{"bluebird":70}],230:[function(require,module,exports){
(function (global){
'use strict';
var createPeerConnection = require('../../helpers/create_peer_connection.js');
var logging = require('../logging.js');
var getStatsAdpater = require('../peer_connection/get_stats_adapter.js');
var getStatsHelpers = require('../peer_connection/get_stats_helpers.js');
var OTHelpers = require('@opentok/ot-helpers');
var OTPlugin = require('@opentok/otplugin.js');
var VideoElementFacade = require('../../helpers/video_element/index.js');
/**
* @returns {Promise.<{packetLostRation: number, roundTripTime: number}>}
*/
module.exports = function webrtcTest(config) {
var _getStats = getStatsAdpater();
var _mediaConfig = config.mediaConfig;
var _localStream = config.localStream;
// todo copied from peer_connection.js
// Normalise these
// var NativeRTCSessionDescription;
var NativeRTCIceCandidate;
if (!OTPlugin.isInstalled()) {
// order is very important: 'RTCSessionDescription' defined in Firefox Nighly but useless
// NativeRTCSessionDescription = (global.mozRTCSessionDescription ||
// global.RTCSessionDescription);
NativeRTCIceCandidate = (global.RTCIceCandidate || global.mozRTCIceCandidate);
} else {
// NativeRTCSessionDescription = OTPlugin.RTCSessionDescription;
NativeRTCIceCandidate = OTPlugin.RTCIceCandidate;
}
function isCandidateRelay(candidate) {
return candidate.candidate.indexOf('relay') !== -1;
}
/**
* Create video a element attaches it to the body and put it visually outside the body.
*
* @returns {VideoElementFacade}
*/
function createVideoElementForTest() {
var videoElement = new VideoElementFacade({ attributes: { muted: true } });
videoElement.domElement().style.position = 'absolute';
videoElement.domElement().style.top = '-9999%';
videoElement.appendTo(document.body);
return videoElement;
}
function createPeerConnectionForTest() {
return new OTHelpers.RSVP.Promise(function(resolve, reject) {
createPeerConnection({
iceServers: _mediaConfig.iceServers
}, {},
null,
function(error, pc) {
if (error) {
reject(new OTHelpers.Error('createPeerConnection failed', 1600, error));
} else {
resolve(pc);
}
}
);
});
}
function createOffer(pc) {
return new OTHelpers.RSVP.Promise(function(resolve, reject) {
pc.createOffer(resolve, reject);
});
}
function attachMediaStream(videoElement, webRtcStream) {
return new OTHelpers.RSVP.Promise(function(resolve, reject) {
videoElement.bindToStream(webRtcStream, function(error) {
if (error) {
reject(new OTHelpers.Error('bindToStream failed', 1600, error));
} else {
resolve();
}
});
});
}
function addIceCandidate(pc, candidate) {
return new OTHelpers.RSVP.Promise(function(resolve, reject) {
pc.addIceCandidate(new NativeRTCIceCandidate({
sdpMLineIndex: candidate.sdpMLineIndex,
candidate: candidate.candidate
}), resolve, reject);
});
}
function setLocalDescription(pc, offer) {
return new OTHelpers.RSVP.Promise(function(resolve, reject) {
pc.setLocalDescription(offer, resolve, function(error) {
reject(new OTHelpers.Error('setLocalDescription failed', 1600, error));
});
});
}
function setRemoteDescription(pc, offer) {
return new OTHelpers.RSVP.Promise(function(resolve, reject) {
pc.setRemoteDescription(offer, resolve, function(error) {
reject(new OTHelpers.Error('setRemoteDescription failed', 1600, error));
});
});
}
function createAnswer(pc) {
return new OTHelpers.RSVP.Promise(function(resolve, reject) {
pc.createAnswer(resolve, function(error) {
reject(new OTHelpers.Error('createAnswer failed', 1600, error));
});
});
}
function getStats(pc) {
return new OTHelpers.RSVP.Promise(function(resolve, reject) {
_getStats(pc, function(error, stats) {
if (error) {
reject(new OTHelpers.Error('geStats failed', 1600, error));
} else {
resolve(stats);
}
});
});
}
function createOnIceCandidateListener(pc) {
return function(event) {
if (event.candidate && isCandidateRelay(event.candidate)) {
addIceCandidate(pc, event.candidate)['catch'](function() {
logging.warn('An error occurred while adding a ICE candidate during webrtc test');
});
}
};
}
/**
* @param {{videoBytesReceived: number, audioBytesReceived: number, startTs: number}} statsSamples
* @returns {number} the bandwidth in bits per second
*/
function calculateBandwidth(statsSamples) {
return (((statsSamples.videoBytesReceived + statsSamples.audioBytesReceived) * 8) /
(OTHelpers.now() - statsSamples.startTs)) * 1000;
}
/**
* @returns {Promise.<{packetLostRation: number, roundTripTime: number}>}
*/
function collectPeerConnectionStats(localPc, remotePc) {
var SAMPLING_DELAY = 1000;
return new OTHelpers.RSVP.Promise(function(resolve) {
var collectionActive = true;
var _statsSamples = {
startTs: OTHelpers.now(),
packetLostRatioSamplesCount: 0,
packetLostRatio: 0,
roundTripTimeSamplesCount: 0,
roundTripTime: 0,
videoBytesReceived: 0,
audioBytesReceived: 0
};
function sample() {
OTHelpers.RSVP.Promise.all([
getStats(localPc).then(function(stats) {
stats.forEach(function(stat) {
if (getStatsHelpers.isVideoStat(stat)) {
var rtt = null;
if (stat.hasOwnProperty('googRtt')) {
rtt = parseInt(stat.googRtt, 10);
} else if (stat.hasOwnProperty('mozRtt')) {
rtt = stat.mozRtt;
}
if (rtt !== null && rtt > -1) {
_statsSamples.roundTripTimeSamplesCount++;
_statsSamples.roundTripTime += rtt;
}
}
});
}),
getStats(remotePc).then(function(stats) {
stats.forEach(function(stat) {
if (getStatsHelpers.isVideoStat(stat)) {
if (stat.hasOwnProperty('packetsReceived') &&
stat.hasOwnProperty('packetsLost')) {
var packetLost = parseInt(stat.packetsLost, 10);
var packetsReceived = parseInt(stat.packetsReceived, 10);
if (packetLost >= 0 && packetsReceived > 0) {
_statsSamples.packetLostRatioSamplesCount++;
_statsSamples.packetLostRatio += packetLost * 100 / packetsReceived;
}
}
if (stat.hasOwnProperty('bytesReceived')) {
_statsSamples.videoBytesReceived = parseInt(stat.bytesReceived, 10);
}
} else if (getStatsHelpers.isAudioStat(stat)) {
if (stat.hasOwnProperty('bytesReceived')) {
_statsSamples.audioBytesReceived = parseInt(stat.bytesReceived, 10);
}
}
});
})
])
.then(function() {
// wait and trigger another round of collection
setTimeout(function() {
if (collectionActive) {
sample();
}
}, SAMPLING_DELAY);
});
}
// start the sampling "loop"
sample();
/**
* @param {boolean} extended marks the test results as extended
*/
function stopCollectStats(extended) {
collectionActive = false;
var pcStats = {
packetLostRatio: _statsSamples.packetLostRatioSamplesCount > 0 ?
_statsSamples.packetLostRatio /= _statsSamples.packetLostRatioSamplesCount * 100 : null,
roundTripTime: _statsSamples.roundTripTimeSamplesCount > 0 ?
_statsSamples.roundTripTime /= _statsSamples.roundTripTimeSamplesCount : null,
bandwidth: calculateBandwidth(_statsSamples),
extended: extended
};
resolve(pcStats);
}
// sample for the nominal delay
// if the bandwidth is bellow the threshold at the end we give an extra time
setTimeout(function() {
if (calculateBandwidth(_statsSamples) < _mediaConfig.thresholdBitsPerSecond) {
// give an extra delay in case it was transient bandwidth problem
setTimeout(function() {
stopCollectStats(true);
}, _mediaConfig.extendedDuration * 1000);
} else {
stopCollectStats(false);
}
}, _mediaConfig.duration * 1000);
});
}
return OTHelpers.RSVP.Promise
.all([createPeerConnectionForTest(), createPeerConnectionForTest()])
.then(function(pcs) {
var localPc = pcs[0];
var remotePc = pcs[1];
var localVideo = createVideoElementForTest();
var remoteVideo = createVideoElementForTest();
attachMediaStream(localVideo, _localStream);
localPc.addStream(_localStream);
var remoteStream;
remotePc.onaddstream = function(evt) {
remoteStream = evt.stream;
attachMediaStream(remoteVideo, remoteStream);
};
localPc.onicecandidate = createOnIceCandidateListener(remotePc);
remotePc.onicecandidate = createOnIceCandidateListener(localPc);
function dispose() {
localVideo.destroy();
remoteVideo.destroy();
localPc.close();
remotePc.close();
}
return createOffer(localPc)
.then(function(offer) {
return OTHelpers.RSVP.Promise.all([
setLocalDescription(localPc, offer),
setRemoteDescription(remotePc, offer)
]);
})
.then(function() {
return createAnswer(remotePc);
})
.then(function(answer) {
return OTHelpers.RSVP.Promise.all([
setLocalDescription(remotePc, answer),
setRemoteDescription(localPc, answer)
]);
})
.then(function() {
return collectPeerConnectionStats(localPc, remotePc);
})
.then(function(value) {
dispose();
return value;
}, function(error) {
dispose();
throw error;
});
});
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"../../helpers/create_peer_connection.js":146,"../../helpers/video_element/index.js":159,"../logging.js":187,"../peer_connection/get_stats_adapter.js":210,"../peer_connection/get_stats_helpers.js":211,"@opentok/ot-helpers":4,"@opentok/otplugin.js":40}],231:[function(require,module,exports){
'use strict';
var analytics = require('./analytics.js');
var ExceptionCodes = require('./exception_codes.js');
var OTError = require('./ot_error.js');
var OTHelpers = require('@opentok/ot-helpers');
var sessionObjects = require('./session/objects.js');
var uuid = require('uuid');
/**
* Report that your app experienced an issue. You can use the issue ID with
* Inspector or when discussing
* an issue with the TokBox support team.
*
* @param completionHandler {Function} A function that is called when the call to this method
* succeeds or fails. This function has two parameters. The first parameter is an
* Error object that is set when the call to the reportIssue()
* method fails (for example, if the client is not connected to the network) or null
* when the call to the reportIssue() method succeeds. The second parameter is set to
* the report ID (a unique string) when the call succeeds.
*
* @method OT.reportIssue
* @memberof OT
*/
module.exports = function reportIssue(completionHandler) {
var reportIssueId = uuid();
var sessionCount = sessionObjects.sessions.length();
var completedLogEventCount = 0;
var errorReported = false;
function logEventCompletionHandler(error) {
if (error) {
if (completionHandler && !errorReported) {
var reportIssueError = new OTError(ExceptionCodes.REPORT_ISSUE_ERROR,
'Error calling OT.reportIssue(). Check the client\'s network connection.');
completionHandler(reportIssueError);
errorReported = true;
}
} else {
completedLogEventCount++;
if (completedLogEventCount >= sessionCount && completionHandler && !errorReported) {
completionHandler(null, reportIssueId);
}
}
}
var eventOptions = {
action: 'ReportIssue',
payload: {
reportIssueId: reportIssueId
}
};
if (sessionCount === 0) {
analytics.logEvent(eventOptions, null, logEventCompletionHandler);
} else {
sessionObjects.sessions.forEach(function(session) {
var individualSessionEventOptions = OTHelpers.extend({
sessionId: session.sessionId,
partnerId: session.sessionInfo ? session.sessionInfo.partnerId : null,
p2p: session.sessionInfo.p2pEnabled,
messagingServer: session.sessionInfo.messagingServer
}, eventOptions);
analytics.logEvent(individualSessionEventOptions, null, logEventCompletionHandler);
});
}
};
},{"./analytics.js":166,"./exception_codes.js":183,"./ot_error.js":208,"./session/objects.js":239,"@opentok/ot-helpers":4,"uuid":137}],232:[function(require,module,exports){
(function (global){
'use strict';
var uuid = require('uuid');
/* global chrome */
module.exports = {
isSupportedInThisBrowser: !!navigator.webkitGetUserMedia && typeof chrome !== 'undefined',
autoRegisters: false,
extensionRequired: true,
getConstraintsShowsPermissionUI: true,
sources: {
screen: true,
application: false,
window: false,
browser: false
},
register: function(extensionID, version) {
if (version === 2) {
return this.registerVersion2(extensionID);
}
return this.registerVersion1(extensionID);
},
registerVersion1: function(extensionID) {
if (!extensionID) {
throw new Error('initChromeScreenSharingExtensionHelper: extensionID is required.');
}
var isChrome = !!navigator.webkitGetUserMedia && typeof chrome !== 'undefined';
var callbackRegistry = {};
var isInstalled = void 0;
var prefix = 'com.tokbox.screenSharing.' + extensionID;
var request = function(method, payload) {
var res = { payload: payload, from: 'jsapi' };
res[prefix] = method;
return res;
};
var addCallback = function(fn, timeToWait) {
var timeout;
var requestId = uuid();
callbackRegistry[requestId] = function() {
clearTimeout(timeout);
timeout = null;
fn.apply(null, arguments);
};
if (timeToWait) {
timeout = setTimeout(function() {
delete callbackRegistry[requestId];
fn(new Error('Timeout waiting for response to request.'));
}, timeToWait);
}
return requestId;
};
var isAvailable = function(callback) {
if (!callback) {
throw new Error('isAvailable: callback is required.');
}
if (!isChrome) {
setTimeout(callback.bind(null, false));
return;
}
if (isInstalled !== void 0) {
setTimeout(callback.bind(null, isInstalled));
} else {
var requestId = addCallback(function(event) {
if (isInstalled !== true) {
isInstalled = (event === 'extensionLoaded');
}
callback(isInstalled);
}, 2000);
var post = request('isExtensionInstalled', { requestId: requestId });
global.postMessage(post, '*');
}
};
var getConstraints = function(source, constraints, callback) {
if (!callback) {
throw new Error('getSourceId: callback is required');
}
isAvailable(function(isInstalled) {
if (!isInstalled) {
return callback(new Error('Extension is not installed'));
}
var requestId = addCallback(function(event, payload) {
if (event === 'permissionDenied') {
callback(new Error('PermissionDeniedError'));
} else {
if (!constraints.video) {
constraints.video = {};
}
if (!constraints.video.mandatory) {
constraints.video.mandatory = {};
}
constraints.video.mandatory.chromeMediaSource = 'desktop';
constraints.video.mandatory.chromeMediaSourceId = payload.sourceId;
callback(void 0, constraints);
}
});
global.postMessage(request('getSourceId', { requestId: requestId, source: source }), '*');
});
};
global.addEventListener('message', function(event) {
if (event.origin !== global.location.origin) {
return;
}
if (!(event.data != null && typeof event.data === 'object')) {
return;
}
if (event.data.from !== 'extension') {
return;
}
var method = event.data[prefix];
var payload = event.data.payload;
if (payload && payload.requestId) {
var callback = callbackRegistry[payload.requestId];
delete callbackRegistry[payload.requestId];
if (callback) {
callback(method, payload);
}
}
if (method === 'extensionLoaded') {
isInstalled = true;
}
});
return {
extensionAPIVersion: 1,
extensionID: extensionID,
isInstalled: isAvailable,
getConstraints: getConstraints
};
},
registerVersion2: function(extensionID) {
var isChrome = typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined';
var isInstalled = function(callback) {
if (!callback) {
throw new Error('isAvailable: callback is required.');
}
if (!isChrome) {
setTimeout(callback.bind(null, false));
return;
}
chrome.runtime.sendMessage(extensionID, {
type: 'isInstalled'
}, null, function(response) {
setTimeout(callback.bind(null, !!response));
});
};
var getConstraints = function(source, constraints, callback) {
if (!callback) {
throw new Error('getSourceId: callback is required');
}
isInstalled(function(installed) {
if (!installed) {
return callback(new Error('Extension is not installed'));
}
chrome.runtime.sendMessage(extensionID, {
type: 'getSourceId',
source: source
}, null, function(data) {
if (data.error === 'permissionDenied') {
callback(new Error('PermissionDeniedError'));
} else if (data.error) {
callback(new Error('UnexpectError: ' + data.error));
} else {
if (!constraints.video) {
constraints.video = {};
}
if (!constraints.video.mandatory) {
constraints.video.mandatory = {};
}
constraints.video.mandatory.chromeMediaSource = 'desktop';
constraints.video.mandatory.chromeMediaSourceId = data.sourceId;
callback(void 0, constraints);
}
});
});
};
return {
extensionAPIVersion: 2,
extensionID: extensionID,
isInstalled: isInstalled,
getConstraints: getConstraints
};
}
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"uuid":137}],233:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
module.exports = {
isSupportedInThisBrowser: OTHelpers.env.name === 'Firefox',
autoRegisters: true,
extensionRequired: false,
extensionInstalled: false,
getConstraintsShowsPermissionUI: false,
sources: {
screen: true,
application: OTHelpers.env.name === 'Firefox' && OTHelpers.env.version >= 34,
window: OTHelpers.env.name === 'Firefox' && OTHelpers.env.version >= 34,
browser: OTHelpers.env.name === 'Firefox' && OTHelpers.env.version >= 38
},
register: function() {
return {
isInstalled: function(callback) {
// The extension sets widow.OTScreenSharing in pages loaded from a matching domain
callback(typeof OTScreenSharing === 'object');
},
getConstraints: function(source, constraints, callback) {
constraints.video = {
mediaSource: source
};
// copy constraints under the video object and removed them from the root constraint object
if (constraints.browserWindow) {
constraints.video.browserWindow = constraints.browserWindow;
delete constraints.browserWindow;
}
if (typeof constraints.scrollWithPage !== 'undefined') {
constraints.video.scrollWithPage = constraints.scrollWithPage;
delete constraints.scrollWithPage;
}
callback(void 0, constraints);
}
};
}
};
},{"@opentok/ot-helpers":4}],234:[function(require,module,exports){
'use strict';
var OTPlugin = require('@opentok/otplugin.js');
module.exports = {
isSupportedInThisBrowser: OTPlugin.isSupported(),
autoRegisters: true,
extensionRequired: true,
getConstraintsShowsPermissionUI: false,
sources: {
screen: true,
application: false,
window: true,
browser: false
},
register: function() {
return {
isInstalled: function(callback) {
callback(OTPlugin.isInstalled());
},
getConstraints: function(source, constraints, callback) {
constraints.video.mandatory.chromeMediaSource = source;
callback(void 0, constraints);
}
};
}
};
},{"@opentok/otplugin.js":40}],235:[function(require,module,exports){
'use strict';
var chromeExtensionHelper = require('./chrome_extension_helper.js');
var firefoxExtensionHelper = require('./firefox_extension_helper.js');
var ieExtensionHelper = require('./ie_extension_helper.js');
var OTHelpers = require('@opentok/ot-helpers');
var screenSharing = {};
module.exports = screenSharing;
screenSharing.extensionByKind = {};
screenSharing.extensionClasses = {};
screenSharing.registerExtensionHelper = function(kind, helper) {
screenSharing.extensionClasses[kind] = helper;
if (helper.autoRegisters && helper.isSupportedInThisBrowser) {
screenSharing.registerExtension(kind);
}
};
/**
* Register a Chrome extension for screen-sharing support.
*
* Use the OT.checkScreenSharingCapability() method to check if an extension is
* required, registered, and installed.
*
* The OpenTok
* screensharing-extensions
* repo includes code for creating an extension for screen-sharing support.
*
* @param {String} kind Set this parameter to "chrome". Currently, you can only
* register a screen-sharing extension for Chrome.
*
* @param {String} id The ID for your Chrome screen-sharing extension. You can find this ID at
* chrome://extensions.
*
* @param {Number} version The version of the Chrome screen-sharing extension from the
* screensharing-extensions repo.
* Set this if you are using version 2 or later. For example, if you are using version 2, set this
* to 2. With version 2, the client can use the extension immediately after installing it, without
* reloading the page.
*
* @see OT.initPublisher()
* @see OT.checkScreenSharingCapability()
* @method OT.registerScreenSharingExtension
* @memberof OT
*/
screenSharing.registerExtension = function(kind) {
var initArgs = Array.prototype.slice.call(arguments, 1);
if (screenSharing.extensionClasses[kind] == null) {
throw new Error('Unsupported kind passed to OT.registerScreenSharingExtension');
}
var x = screenSharing.extensionClasses[kind]
.register.apply(screenSharing.extensionClasses[kind], initArgs);
screenSharing.extensionByKind[kind] = x;
};
var screenSharingPickHelper = function() {
var foundClass = OTHelpers.find(Object.keys(screenSharing.extensionClasses), function(cls) {
return screenSharing.extensionClasses[cls].isSupportedInThisBrowser;
});
if (foundClass === void 0) {
return {};
}
return {
name: foundClass,
proto: screenSharing.extensionClasses[foundClass],
instance: screenSharing.extensionByKind[foundClass]
};
};
screenSharing.pickHelper = function() {
return screenSharingPickHelper();
};
/**
* Checks for support for publishing screen-sharing streams on the client browser. The object
* passed to the callback function defines whether screen sharing is supported, as well as whether
* an extension is required, installed, and registered (if needed).
*
*
* OT.checkScreenSharingCapability(function(response) {
* if (!response.supported || response.extensionRegistered === false) {
* // This browser does not support screen sharing
* } else if (response.extensionInstalled === false) {
* // Prompt to install the extension
* } else {
* // Screen sharing is available.
* }
* });
*
* * To publish a screen-sharing video in Chrome or Firefox, the client adds an extension * that enables publishing a screen-sharing video stream on your domain. The OpenTok plugin for * Internet Explorer has screen-sharing capabilities built-in. The OpenTok * screensharing-extensions * sample includes code for creating Chrome and Firefox extensions for screen-sharing support. *
* For support in Firefox, instead of using an extension, you can have Mozilla add your domain * to the Firefox screen-sharing whitelist. *
* For more information, see the * OpenTok Screen Sharing developer guide. * * @param {function} callback The callback invoked with the support options object passed as * the parameter. This object has the following properties that indicate support for publishing * screen-sharing streams in the client: *
*
extensionInstalled (Boolean) — In Chrome, this is set to
* true if the extension is installed and registered. In Firefox, this is set to
* true if the extension is installed and it is set to work in the current page's
* domain. (This requires the Firefox extension to be built using the version of the
* OpenTok screen-sharing
* extension released with OpenTok.js 2.8.0 or later. For previous versions of the
* extension, the extensionInstalled property is always false in
* Firefox.) However, if you register the domain in the Firefox screen-sharing whitelist
* (instead using a Firefox screen-sharing extension), this property is set to
* false; check the client's Firefox version
* (navigator.userAgent.match(/Firefox\/(\d+)/i)[1]) to see if it is greater than
* or equal to the version in which your domain was added to the Firefox whitelist.
* This property is undefined in Internet Explorer.
* supported (Boolean) — Set to true if screen sharing is supported in the
* browser. Check the extensionRequired property to see if the browser requires
* an extension for screen sharing.
* supportedSources (Object) — An object with the following properties:
* application, screen, and window. Each property is
* a Boolean value indicating support. In Firefox, each of these properties is set to
* true. Currently in Chrome, only the screen property is
* set to true. In Internet Explorer (using the OpenTok plugin), only
* screen and window are set to true.
* The options parameter also includes the following properties, which apply to screen-sharing * support in Chrome (in all other browsers these properties are undefined): *
extensionRequired (String) — Set to "chrome" in Chrome,
* which requires a screen-sharing extension to be installed. This property is undefined in
* other browsers.
* extensionRegistered (Boolean) — In Chrome, this property is set to
* true if a screen-sharing extension is registered; otherwise it is set to
* false. In other browsers (which do not require an extension), this property is
* undefined. Use the OT.registerScreenSharingExtension() method to register a
* screen-sharing extension in Chrome.
* OT.initSession() method provides access to
* much of the OpenTok functionality.
*
* @class Session
* @augments EventDispatcher
*
* @property {Capabilities} capabilities A {@link Capabilities} object that includes information
* about the capabilities of the client. All properties of the capabilities object
* are undefined until you have connected to a session and the completion handler for the
* Session.connect() method has been called without error.
* @property {Connection} connection The {@link Connection} object for this session. The
* connection property is only available once the the completion handler for the
* Session.connect() method has been called successfully. See the
* Session.connect() method and the {@link Connection} class.
* @property {String} sessionId The session ID for this session. You pass this value into the
* OT.initSession() method when you create the Session object. (Note: a Session
* object is not connected to the OpenTok server until you call the connect() method of the
* object and its completion handler is called without error. See the
* OT.initSession() and the Session.connect()
* methods.) For more information on sessions and session IDs, see
* Session creation.
*/
SessionHandle.Session = function(apiKey, sessionId) {
OTHelpers.eventing(this);
this._tag = sessionTag;
// Check that the client meets the minimum requirements, if they don't the upgrade
// flow will be triggered.
if (!systemRequirements.check()) {
systemRequirements.upgrade();
return;
}
if (sessionId == null) {
sessionId = apiKey;
apiKey = null;
}
this.id = this.sessionId = sessionId;
var _socket, _connectivityAttemptPinger, _token, _p2p, _messagingServer, _attemptStartTime;
var _initialConnection = true;
var _apiKey = apiKey;
var _session = this;
var _sessionId = sessionId;
var _widgetId = uuid();
var _connectionId = uuid();
var setState = OTHelpers.statable(this, [
'disconnected', 'connecting', 'connected', 'disconnecting'
], 'disconnected');
this.connection = null;
this.connections = new OTHelpers.Collection();
this.streams = new OTHelpers.Collection();
this.archives = new OTHelpers.Collection();
//--------------------------------------
// MESSAGE HANDLERS
//--------------------------------------
// The duplication of this and sessionConnectionFailed will go away when
// session and messenger are refactored
var sessionConnectFailed = function(reason, code) {
setState('disconnected');
logging.error(reason);
this.trigger('sessionConnectFailed',
new OTError(code || ExceptionCodes.CONNECT_FAILED, reason));
OTError.handleJsException(reason, code || ExceptionCodes.CONNECT_FAILED, {
session: this
});
};
var sessionDisconnectedHandler = function(event) {
var reason = event.reason;
if (reason === 'networkTimedout') {
reason = 'networkDisconnected';
this.logEvent('Connect', 'TimeOutDisconnect', { reason: event.reason });
} else {
this.logEvent('Connect', 'Disconnected', { reason: event.reason });
}
var publicEvent = new Events.SessionDisconnectEvent('sessionDisconnected', reason);
reset();
disconnectComponents.call(this, reason);
var defaultAction = function() {
// Although part of the defaultAction for sessionDisconnected we have
// chosen to still destroy Publishers within the session as there is
// another mechanism to stop a Publisher from being destroyed.
// Publishers use preventDefault on the Publisher streamDestroyed event
destroyPublishers.call(this, publicEvent.reason);
if (!publicEvent.isDefaultPrevented()) {
destroySubscribers.call(this, publicEvent.reason);
}
}.bind(this);
this.dispatchEvent(publicEvent, defaultAction);
};
var connectionCreatedHandler = function(connection) {
// We don't broadcast events for the symphony connection
if (connection.id.match(/^symphony\./)) { return; }
this.dispatchEvent(new Events.ConnectionEvent(
Events.Event.names.CONNECTION_CREATED,
connection
));
};
var connectionDestroyedHandler = function(connection, reason) {
// We don't broadcast events for the symphony connection
if (connection.id.match(/^symphony\./)) { return; }
// Don't delete the connection if it's ours. This only happens when
// we're about to receive a session disconnected and session disconnected
// will also clean up our connection.
if (connection.id === _socket.id()) { return; }
this.dispatchEvent(
new Events.ConnectionEvent(
Events.Event.names.CONNECTION_DESTROYED,
connection,
reason
)
);
};
var streamCreatedHandler = function(stream) {
if (stream.connection.id !== this.connection.id) {
this.dispatchEvent(new Events.StreamEvent(
Events.Event.names.STREAM_CREATED,
stream,
null,
false
));
}
};
var streamPropertyModifiedHandler = function(event) {
var stream = event.target;
var propertyName = event.changedProperty;
var newValue = event.newValue;
if (propertyName === 'videoDisableWarning' || propertyName === 'audioDisableWarning') {
return; // These are not public properties, skip top level event for them.
}
if (propertyName === 'videoDimensions') {
newValue = { width: newValue.width, height: newValue.height };
}
this.dispatchEvent(new Events.StreamPropertyChangedEvent(
Events.Event.names.STREAM_PROPERTY_CHANGED,
stream,
propertyName,
event.oldValue,
newValue
));
};
var streamDestroyedHandler = function(stream, reason) {
// if the stream is one of ours we delegate handling
// to the publisher itself.
if (stream.connection.id === this.connection.id) {
sessionObjects.publishers.where({ streamId: stream.id }).forEach(function(publisher) {
publisher._.unpublishFromSession(this, reason);
}, this);
return;
}
var event = new Events.StreamEvent('streamDestroyed', stream, reason, true);
var defaultAction = function() {
if (!event.isDefaultPrevented()) {
// If we are subscribed to any of the streams we should unsubscribe
sessionObjects.subscribers.where({ streamId: stream.id }).forEach(function(subscriber) {
if (subscriber.session.id === this.id) {
if (subscriber.stream) {
subscriber.destroy('streamDestroyed');
}
}
},
this
);
}
// @TODO Add a else with a one time warning that this no longer cleans up the publisher
}.bind(this);
this.dispatchEvent(event, defaultAction);
};
var archiveCreatedHandler = function(archive) {
this.dispatchEvent(new Events.ArchiveEvent('archiveStarted', archive));
};
var archiveDestroyedHandler = function(archive) {
this.dispatchEvent(new Events.ArchiveEvent('archiveDestroyed', archive));
};
var archiveUpdatedHandler = function(event) {
var archive = event.target;
var propertyName = event.changedProperty;
var newValue = event.newValue;
if (propertyName === 'status' && newValue === 'stopped') {
this.dispatchEvent(new Events.ArchiveEvent('archiveStopped', archive));
} else {
this.dispatchEvent(new Events.ArchiveEvent('archiveUpdated', archive));
}
};
var init = function() {
_session.token = _token = null;
setState('disconnected');
_session.connection = null;
_session.capabilities = new Capabilities([]);
_session.connections.destroy();
_session.streams.destroy();
_session.archives.destroy();
};
// Put ourselves into a pristine state
var reset = function() {
// reset connection id now so that following calls to testNetwork and connect will share
// the same new session id. We need to reset here because testNetwork might be call after
// and it is always called before the session is connected
// on initial connection we don't reset
_connectionId = uuid();
init();
};
var disconnectComponents = function(reason) {
sessionObjects.publishers.where({ session: this }).forEach(function(publisher) {
publisher.disconnect(reason);
});
sessionObjects.subscribers.where({ session: this }).forEach(function(subscriber) {
subscriber.disconnect();
});
};
var destroyPublishers = function(reason) {
sessionObjects.publishers.where({ session: this }).forEach(function(publisher) {
publisher._.streamDestroyed(reason);
});
};
var destroySubscribers = function(reason) {
sessionObjects.subscribers.where({ session: this }).forEach(function(subscriber) {
subscriber.destroy(reason);
});
};
var connectMessenger = function() {
logging.debug('OT.Session: connecting to Raptor');
var socketUrl = this.sessionInfo.messagingURL;
var symphonyUrl = properties.symphonyAddresss || this.sessionInfo.symphonyAddress;
_socket = new SessionHandle.Session.RaptorSocket(
_connectionId,
_widgetId,
socketUrl,
symphonyUrl,
SessionDispatcher(this)
);
_socket.connect(_token, this.sessionInfo, function(error, sessionState) {
if (error) {
var payload = omit(error, ['code', 'message', 'reason']);
var options;
if (error.code || error.message || error.reason) {
options = {
failureCode: error.code,
failureMessage: error.message,
failureReason: error.reason
};
}
_socket = void 0;
this.logConnectivityEvent('Failure', payload, options);
sessionConnectFailed.call(this, error.message, error.code);
return;
}
logging.debug('OT.Session: Received session state from Raptor', sessionState);
this.connection = this.connections.get(_socket.id());
if (this.connection) {
this.capabilities = this.connection.permissions;
}
setState('connected');
this.logConnectivityEvent('Success', { connectionId: this.connection.id });
// Listen for our own connection's destroyed event so we know when we've been disconnected.
this.connection.on('destroyed', sessionDisconnectedHandler, this);
// Listen for connection updates
this.connections.on({
add: connectionCreatedHandler,
remove: connectionDestroyedHandler
}, this);
// Listen for stream updates
this.streams.on({
add: streamCreatedHandler,
remove: streamDestroyedHandler,
update: streamPropertyModifiedHandler
}, this);
this.archives.on({
add: archiveCreatedHandler,
remove: archiveDestroyedHandler,
update: archiveUpdatedHandler
}, this);
this.dispatchEvent(
new Events.SessionConnectEvent(Events.Event.names.SESSION_CONNECTED),
function() {
this.connections._triggerAddEvents(); // { id: this.connection.id }
this.streams._triggerAddEvents(); // { id: this.stream.id }
this.archives._triggerAddEvents();
}.bind(this)
);
}.bind(this));
};
var getSessionInfo = function() {
if (this.is('connecting')) {
var session = this;
this.logEvent('SessionInfo', 'Attempt');
SessionInfo.get(sessionId, _token).then(
onSessionInfoResponse,
function(error) {
session.logConnectivityEvent('Failure', null, {
failureReason: 'GetSessionInfo',
failureCode: (error.code || 'No code'),
failureMessage: error.message
});
sessionConnectFailed.call(session,
error.message + (error.code ? ' (' + error.code + ')' : ''),
error.code);
}
);
}
};
var onSessionInfoResponse = function(sessionInfo) {
if (OTPlugin.hasOwnProperty('settings')) {
OTPlugin.settings.rememberDeviceChoice = sessionInfo.rememberDeviceChoiceIE;
}
if (this.is('connecting')) {
this.sessionInfo = sessionInfo;
_p2p = sessionInfo.p2pEnabled;
_messagingServer = sessionInfo.messagingServer;
this.logEvent('SessionInfo', 'Success', null, {
features: {
reconnection: OTHelpers.castToBoolean(sessionInfo.reconnection),
renegotiation: OTHelpers.castToBoolean((OTHelpers.hasCapabilities('iceRestarts') &&
sessionInfo.renegotiation)),
simulcast: sessionInfo.simulcast === undefined ? false :
sessionInfo.simulcast && OTHelpers.env.name === 'Chrome'
}
}, {
messagingServer: sessionInfo.messagingServer
});
var overrides = properties.sessionInfoOverrides;
if (overrides != null && typeof overrides === 'object') {
this.sessionInfo = OTHelpers.defaults(overrides, this.sessionInfo);
}
if (this.sessionInfo.partnerId && this.sessionInfo.partnerId !== _apiKey) {
this.apiKey = _apiKey = this.sessionInfo.partnerId;
var reason = 'Authentication Error: The API key does not match the token or session.';
this.logEvent('SessionInfo', 'Failure', null, {
failureCode: ExceptionCodes.AUTHENTICATION_ERROR,
failureReason: 'Authentication',
failureMessage: reason
});
sessionConnectFailed.call(this, reason, ExceptionCodes.AUTHENTICATION_ERROR);
} else {
connectMessenger.call(this);
}
}
}.bind(this);
// Check whether we have permissions to perform the action.
var permittedTo = function(action) {
return this.capabilities.permittedTo(action);
}.bind(this);
// This is a placeholder until error handling can be rewritten
var dispatchError = function(code, message, completionHandler) {
logging.error(code, message);
if (completionHandler && OTHelpers.isFunction(completionHandler)) {
completionHandler.call(null, new OTError(code, message));
}
OTError.handleJsException(message, code, {
session: this
});
}.bind(this);
this.logEvent = function(action, variation, payload, options) {
var event = {
action: action,
variation: variation,
payload: payload,
sessionId: _sessionId,
messagingServer: _messagingServer,
p2p: _p2p,
partnerId: _apiKey
};
event.connectionId = _connectionId;
if (options) { event = OTHelpers.extend(options, event); }
analytics.logEvent(event);
};
/**
* @typedef {Object} Stats
* @property {number} bytesSentPerSecond
* @property {number} bytesReceivedPerSecond
* @property {number} packetLossRatio
* @property {number} rtt
*/
function getTestNetworkConfig(token) {
return new Bluebird.Promise(function(resolve, reject) {
OTHelpers.getJSON(
[properties.apiURL, '/v2/partner/', _apiKey, '/session/', _sessionId, '/connection/',
_connectionId, '/testNetworkConfig'].join(''),
{
headers: { 'X-TB-TOKEN-AUTH': token }
},
function(errorEvent, response) {
if (errorEvent) {
var error = JSON.parse(errorEvent.target.responseText);
if (error.code === -1) {
reject(new OTHelpers.Error('Unexpected HTTP error codes ' +
errorEvent.target.status, '2001'));
} else if (error.code === 10001 || error.code === 10002) {
reject(new OTHelpers.Error(error.message, '1004'));
} else {
reject(new OTHelpers.Error(error.message, error.code));
}
} else {
resolve(response);
}
});
});
}
/**
* @param {string} token
* @param {Publisher} publisher
* @param {function(?OTHelpers.Error, Stats=)} callback
*/
this.testNetwork = function(token, publisher, callback) {
_session.logEvent('TestNetwork');
if (this.isConnected()) {
callback(new OTHelpers.Error('Session connected, cannot test network', 1015));
return;
}
var webRtcStreamPromise = new Bluebird.Promise(
function(resolve, reject) {
var webRtcStream = publisher._.webRtcStream();
if (webRtcStream) {
resolve(webRtcStream);
} else {
var onAccessAllowed = function() {
unbind();
resolve(publisher._.webRtcStream());
};
var onPublishComplete = function(error) {
if (error) {
unbind();
reject(error);
}
};
var unbind = function() {
publisher.off('publishComplete', onPublishComplete);
publisher.off('accessAllowed', onAccessAllowed);
};
publisher.on('publishComplete', onPublishComplete);
publisher.on('accessAllowed', onAccessAllowed);
}
});
var testConfig, webrtcStats;
Bluebird.Promise.all([getTestNetworkConfig(token), webRtcStreamPromise])
.then(function(values) {
var webRtcStream = values[1];
testConfig = values[0];
return webrtcTest({ mediaConfig: testConfig.media, localStream: webRtcStream });
})
.then(function(stats) {
logging.debug('Received stats from webrtcTest: ', stats);
if (stats.bandwidth < testConfig.media.thresholdBitsPerSecond) {
return Bluebird.Promise.reject(new OTHelpers.Error(
'The detect bandwidth from the WebRTC stage of the test was not sufficient to run ' +
'the HTTP stage of the test', 1553
));
}
webrtcStats = stats;
})
.then(function() {
// run the HTTP test only if the PC test was not extended
if (!webrtcStats.extended) {
return httpTest({ httpConfig: testConfig.http });
}
})
.then(function(httpStats) {
var stats = {
uploadBitsPerSecond: httpStats ? httpStats.uploadBandwidth : webrtcStats.bandwidth,
downloadBitsPerSecond: httpStats ? httpStats.downloadBandwidth : webrtcStats.bandwidth,
packetLossRatio: webrtcStats.packetLostRatio,
roundTripTimeMilliseconds: webrtcStats.roundTripTime
};
callback(null, stats);
// IE9 (ES3 JS engine) requires bracket notation for "catch" keyword
})['catch'](function(error) {
callback(error);
});
};
this.logConnectivityEvent = function(variation, payload, options) {
if (variation === 'Attempt' || !_connectivityAttemptPinger) {
_attemptStartTime = new Date().getTime();
var pingerOptions = {
action: 'Connect',
sessionId: _sessionId,
p2p: this.sessionInfo ? this.sessionInfo.p2pEnabled : null,
messagingServer: this.sessionInfo ? this.sessionInfo.messagingServer : null,
partnerId: _apiKey
};
if (this.connection && this.connection.id) {
pingerOptions = event.connectionId = this.connection.id;
} else if (_connectionId) {
pingerOptions.connectionId = _connectionId;
}
_connectivityAttemptPinger = new ConnectivityAttemptPinger(pingerOptions);
}
_connectivityAttemptPinger.setVariation(variation);
if (variation === 'Failure' || variation === 'Success' || variation === 'Cancel') {
if (!options) { options = {}; }
OTHelpers.extend(options, {
attemptDuration: new Date().getTime() - _attemptStartTime
});
}
this.logEvent('Connect', variation, payload, options);
};
/**
* Connects to an OpenTok session.
* * Upon a successful connection, the completion handler (the second parameter of the method) is * invoked without an error object passed in. (If there is an error connecting, the completion * handler is invoked with an error object.) Make sure that you have successfully connected to the * session before calling other methods of the Session object. *
*
* The Session object dispatches a connectionCreated event when any client
* (including your own) connects to to the session.
*
* The following code initializes a session and sets up an event listener for when the session * connects: *
*
* var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
* var sessionID = ""; // Replace with your own session ID.
* // See https://dashboard.tokbox.com/projects
* var token = ""; // Replace with a generated token that has been assigned the moderator role.
* // See https://dashboard.tokbox.com/projects
*
* var session = OT.initSession(apiKey, sessionID);
* session.connect(token, function(error) {
* if (error) {
* console.log(error.message);
* } else {
* // You have connected to the session. You could publish a stream now.
* }
* });
*
* * *
* exception (ExceptionEvent) Dispatched
* by the OT class locally in the event of an error.
*
* connectionCreated (ConnectionEvent)
* Dispatched by the Session object on all clients connected to the session.
*
* sessionConnected (SessionConnectEvent)
* Dispatched locally by the Session object when the connection is established. However,
* you can pass a completion handler function in as the second parameter of the
* connect() and use this function instead of a listener for the
* sessionConnected event.
*
connect() method succeeds or fails. This function takes one parameter —
* error (see the Error object).
* On success, the completionHandler function is not passed any
* arguments. On error, the function is passed an error object parameter
* (see the Error object). The
* error object has two properties: code (an integer) and
* message (a string), which identify the cause of the failure. The following
* code adds a completionHandler when calling the connect() method:
*
* session.connect(token, function (error) {
* if (error) {
* console.log(error.message);
* } else {
* console.log("Connected to session.");
* }
* });
*
*
* Note that upon connecting to the session, the Session object dispatches a
* sessionConnected event in addition to calling the completionHandler.
* The SessionConnectEvent object, which defines the sessionConnected event,
* includes connections and streams properties, which
* list the connections and streams in the session when you connect.
*
* Calling the disconnect() method ends your connection with the session. In the
* course of terminating your connection, it also ceases publishing any stream(s) you were
* publishing.
*
* Session objects on remote clients dispatch streamDestroyed events for any
* stream you were publishing. The Session object dispatches a sessionDisconnected
* event locally. The Session objects on remote clients dispatch connectionDestroyed
* events, letting other connections know you have left the session. The
* {@link SessionDisconnectEvent} and {@link StreamEvent} objects that define the
* sessionDisconnect and connectionDestroyed events each have a
* reason property. The reason property lets the developer determine
* whether the connection is being terminated voluntarily and whether any streams are being
* destroyed as a byproduct of the underlying connection's voluntary destruction.
*
* If the session is not currently connected, calling this method causes a warning to be logged. * See OT.setLogLevel(). *
* *
* Note: If you intend to reuse a Publisher object created using
* OT.initPublisher() to publish to different sessions sequentially, call either
* Session.disconnect() or Session.unpublish(). Do not call both.
* Then call the preventDefault() method of the Publisher's streamDestroyed
* event object to prevent the Publisher object from being removed from the page. Be sure to
* call preventDefault() only if the connection.connectionId property
* of the Stream object in the event matches the connection.connectionId property of
* your Session object (to ensure that you are preventing the default behavior for your published
* streams, not for other streams that you subscribe to).
*
* sessionDisconnected
* (SessionDisconnectEvent)
* Dispatched locally when the connection is disconnected.
*
* connectionDestroyed (ConnectionEvent)
* Dispatched on other clients, along with the streamDestroyed event (as warranted).
*
* streamDestroyed (StreamEvent)
* Dispatched on other clients if streams are lost as a result of the session disconnecting.
*
publish() method starts publishing an audio-video stream to the session.
* The audio-video stream is captured from a local microphone and webcam. Upon successful
* publishing, the Session objects on all connected clients dispatch the
* streamCreated event.
*
*
*
* You pass a Publisher object as the one parameter of the method. You can initialize a
* Publisher object by calling the OT.initPublisher()
* method. Before calling Session.publish().
*
This method takes an alternate form: publish([targetElement:String,
* properties:Object]):Publisher In this form, you do not pass a Publisher
* object into the function. Instead, you pass in a targetElement (the ID of the
* DOM element that the Publisher will replace) and a properties object that
* defines options for the Publisher (see OT.initPublisher().)
* The method returns a new Publisher object, which starts sending an audio-video stream to the
* session. The remainder of this documentation describes the form that takes a single Publisher
* object as a parameter.
*
*
* A local display of the published stream is created on the web page by replacing * the specified element in the DOM with a streaming video display. The video stream * is automatically mirrored horizontally so that users see themselves and movement * in their stream in a natural way. If the width and height of the display do not match * the 4:3 aspect ratio of the video signal, the video stream is cropped to fit the * display. *
* ** If calling this method creates a new Publisher object and the OpenTok library does not * have access to the camera or microphone, the web page alerts the user to grant access * to the camera and microphone. *
* *
* The OT object dispatches an exception event if the user's role does not
* include permissions required to publish. For example, if the user's role is set to subscriber,
* then they cannot publish. You define a user's role when you create the user token using the
* generate_token() method of the
* OpenTok server-side
* libraries or the Dashboard page.
* You pass the token string as a parameter of the connect() method of the Session
* object. See ExceptionEvent and
* OT.on().
*
* The application throws an error if the session is not connected. *
* *
* exception (ExceptionEvent) Dispatched
* by the OT object. This can occur when user's role does not allow publishing (the
* code property of event object is set to 1500); it can also occur if the c
* onnection fails to connect (the code property of event object is set to 1013).
* WebRTC is a peer-to-peer protocol, and it is possible that connections will fail to connect.
* The most common cause for failure is a firewall that the protocol cannot traverse.
*
* streamCreated (StreamEvent)
* The stream has been published. The Session object dispatches this on all clients
* subscribed to the stream, as well as on the publisher's client.
*
* The following example publishes a video once the session connects: *
*
* var sessionId = ""; // Replace with your own session ID.
* // See https://dashboard.tokbox.com/projects
* var token = ""; // Replace with a generated token that has been assigned the moderator role.
* // See https://dashboard.tokbox.com/projects
* var session = OT.initSession(apiKey, sessionID);
* session.connect(token, function(error) {
* if (error) {
* console.log(error.message);
* } else {
* var publisherOptions = {width: 400, height:300, name:"Bob's stream"};
* // This assumes that there is a DOM element with the ID 'publisher':
* publisher = OT.initPublisher('publisher', publisherOptions);
* session.publish(publisher);
* }
* });
*
*
* @param {Publisher} publisher A Publisher object, which you initialize by calling the
* OT.initPublisher() method.
*
* @param {Function} completionHandler (Optional) A function to be called when the call to the
* publish() method succeeds or fails. This function takes one parameter —
* error. On success, the completionHandler function is not passed any
* arguments. On error, the function is passed an error object parameter
* (see the Error object). The
* error object has two properties: code (an integer) and
* message (a string), which identify the cause of the failure. Calling
* publish() fails if the role assigned to your token is not "publisher" or
* "moderator"; in this case error.code is set to 1500. Calling
* publish() also fails the client fails to connect; in this case
* error.code is set to 1013. The following code adds a
* completionHandler when calling the publish() method:
*
* session.publish(publisher, null, function (error) {
* if (error) {
* console.log(error.message);
* } else {
* console.log("Publishing a stream.");
* }
* });
*
*
* @returns The Publisher object for this stream.
*
* @method #publish
* @memberOf Session
*/
this.publish = function(publisher, properties, completionHandler) {
if (typeof publisher === 'function') {
completionHandler = publisher;
publisher = undefined;
}
if (typeof properties === 'function') {
completionHandler = properties;
properties = undefined;
}
if (this.isNot('connected')) {
analytics.logError(
1010,
'OT.exception',
'We need to be connected before you can publish',
null,
{
action: 'Publish',
variation: 'Failure',
failureReason: 'unconnected',
failureCode: ExceptionCodes.NOT_CONNECTED,
failureMessage: 'We need to be connected before you can publish',
sessionId: _sessionId,
streamId: (publisher && publisher.stream) ? publisher.stream.id : null,
p2p: this.sessionInfo ? this.sessionInfo.p2pEnabled : undefined,
messagingServer: this.sessionInfo ? this.sessionInfo.messagingServer : null,
partnerId: _apiKey
}
);
if (completionHandler && OTHelpers.isFunction(completionHandler)) {
dispatchError(ExceptionCodes.NOT_CONNECTED,
'We need to be connected before you can publish', completionHandler);
}
return null;
}
if (!permittedTo('publish')) {
var errorMessage = 'This token does not allow publishing. The role must be at least ' +
'`publisher` to enable this functionality';
var options = {
failureReason: 'Permission',
failureCode: ExceptionCodes.UNABLE_TO_PUBLISH,
failureMessage: errorMessage
};
this.logEvent('Publish', 'Failure', null, options);
dispatchError(ExceptionCodes.UNABLE_TO_PUBLISH, errorMessage, completionHandler);
return null;
}
// If the user has passed in an ID of a element then we create a new publisher.
if (!publisher || typeof (publisher) === 'string' || OTHelpers.isElementNode(publisher)) {
// Initiate a new Publisher with the new session credentials
publisher = initPublisher(publisher, properties);
} else if (publisher instanceof Publisher) {
// If the publisher already has a session attached to it we can
if ('session' in publisher && publisher.session && 'sessionId' in publisher.session) {
// send a warning message that we can't publish again.
if (publisher.session.sessionId === this.sessionId) {
logging.warn('Cannot publish ' + publisher.guid() + ' again to ' +
this.sessionId + '. Please call session.unpublish(publisher) first.');
} else {
logging.warn('Cannot publish ' + publisher.guid() + ' publisher already attached to ' +
publisher.session.sessionId + '. Please call session.unpublish(publisher) first.');
}
}
} else {
dispatchError(ExceptionCodes.UNABLE_TO_PUBLISH,
'Session.publish :: First parameter passed in is neither a ' +
'string nor an instance of the Publisher',
completionHandler);
return undefined;
}
publisher.once('publishComplete', function() {
var args = Array.prototype.slice.call(arguments);
var err = args[0];
if (err) {
err.message = 'Session.publish :: ' + err.message;
args[0] = err;
logging.error(err.code, err.message);
}
if (completionHandler && OTHelpers.isFunction(completionHandler)) {
completionHandler.apply(null, args);
}
});
// Add publisher reference to the session
publisher._.publishToSession(this);
// return the embed publisher
return publisher;
};
/**
* Ceases publishing the specified publisher's audio-video stream
* to the session. By default, the local representation of the audio-video stream is
* removed from the web page. Upon successful termination, the Session object on every
* connected web page dispatches
* a streamDestroyed event.
*
*
*
* To prevent the Publisher from being removed from the DOM, add an event listener for the
* streamDestroyed event dispatched by the Publisher object and call the
* preventDefault() method of the event object.
*
* Note: If you intend to reuse a Publisher object created using
* OT.initPublisher() to publish to different sessions sequentially, call
* either Session.disconnect() or Session.unpublish(). Do not call
* both. Then call the preventDefault() method of the streamDestroyed
* or sessionDisconnected event object to prevent the Publisher object from being
* removed from the page. Be sure to call preventDefault() only if the
* connection.connectionId property of the Stream object in the event matches the
* connection.connectionId property of your Session object (to ensure that you are
* preventing the default behavior for your published streams, not for other streams that you
* subscribe to).
*
* streamDestroyed (StreamEvent)
* The stream associated with the Publisher has been destroyed. Dispatched on by the
* Publisher on on the Publisher's browser. Dispatched by the Session object on
* all other connections subscribing to the publisher's stream.
*
* <script>
* var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
* var sessionID = ""; // Replace with your own session ID.
* // See https://dashboard.tokbox.com/projects
* var token = "Replace with the TokBox token string provided to you."
* var publisher;
* var session = OT.initSession(apiKey, sessionID);
* session.connect(token, function(error) {
* if (error) {
* console.log(error.message);
* } else {
* // This assumes that there is a DOM element with the ID 'publisher':
* publisher = OT.initPublisher('publisher');
* session.publish(publisher);
* }
* });
*
* function unpublish() {
* session.unpublish(publisher);
* }
* </script>
*
* <body>
*
* <div id="publisherContainer/>
* <br/>
*
* <a href="javascript:unpublish()">Stop Publishing</a>
*
* </body>
*
*
*
* @see publish()
*
* @see streamDestroyed event
*
* @param {Publisher} publisher The Publisher object to stop streaming.
*
* @method #unpublish
* @memberOf Session
*/
this.unpublish = function(publisher) {
if (!publisher) {
logging.error('OT.Session.unpublish: publisher parameter missing.');
return;
}
// Unpublish the localMedia publisher
publisher._.unpublishFromSession(this, 'unpublished');
};
/**
* Subscribes to a stream that is available to the session. You can get an array of
* available streams from the streams property of the sessionConnected
* and streamCreated events (see
* SessionConnectEvent and
* StreamEvent).
*
* * The subscribed stream is displayed on the local web page by replacing the specified element * in the DOM with a streaming video display. If the width and height of the display do not * match the 4:3 aspect ratio of the video signal, the video stream is cropped to fit * the display. If the stream lacks a video component, a blank screen with an audio indicator * is displayed in place of the video stream. *
* *
* The application throws an error if the session is not connected or if the
* targetElement does not exist in the HTML DOM.
*
* var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
* var sessionID = ""; // Replace with your own session ID.
* // See https://dashboard.tokbox.com/projects
*
* var session = OT.initSession(apiKey, sessionID);
* session.on("streamCreated", function(event) {
* subscriber = session.subscribe(event.stream, targetElement);
* });
* session.connect(token);
*
*
* @param {Stream} stream The Stream object representing the stream to which we are trying to
* subscribe.
*
* @param {Object} targetElement (Optional) The DOM element or the id attribute of
* the existing DOM element used to determine the location of the Subscriber video in the HTML
* DOM. See the insertMode property of the properties parameter. If
* you do not specify a targetElement, the application appends a new DOM element
* to the HTML body.
*
* @param {Object} properties This is an object that contains the following properties:
* audioVolume (Number) The desired audio volume, between 0 and
* 100, when the Subscriber is first opened (default: 50). After you subscribe to the
* stream, you can adjust the volume by calling the
* setAudioVolume() method of the
* Subscriber object. This volume setting affects local playback only; it does not affect
* the stream's volume on other clients.fitMode (String) Determines how the video is displayed if the its
* dimensions do not match those of the DOM element. You can set this property to one of
* the following values:
* *
"cover" — The video is cropped if its dimensions do not match
* those of the DOM element. This is the default setting for videos that have a
* camera as the source (for Stream objects with the videoType property
* set to "camera").
* "contain" — The video is letterboxed if its dimensions do not
* match those of the DOM element. This is the default setting for screen-sharing
* videos (for Stream objects with the videoType property set to
* "screen").
* height (Number) The desired initial height of the displayed
* video in the HTML page (default: 198 pixels). You can specify the number of pixels as
* either a number (such as 300) or a string ending in "px" (such as "300px"). Or you can
* specify a percentage of the size of the parent element, with a string ending in "%"
* (such as "100%"). Note: To resize the video, adjust the CSS of the subscriber's
* DOM element (the element property of the Subscriber object) or (if the
* height is specified as a percentage) its parent DOM element (see
*
* Resizing or repositioning a video).
* true, the default) or not (false). The default UI element
* contains user interface controls, a video loading indicator, and automatic video cropping
* or letterboxing, in addition to the video. (If you leave insertDefaultUI
* set to true, you can control individual UI settings using the
* fitMode, showControls, and style options.)
*
* If you set this option to false, OpenTok.js does not insert a default UI
* element in the HTML DOM, and the element property of the Subscriber object is
* undefined. The Subscriber object dispatches a
* videoElementCreated event when
* the video element (or in Internet Explorer the object element
* containing the video) is created. The element property of the event object
* is a reference to the Subscriber's video (or object) element.
* Add it to the HTML DOM to display the video.
*
* Set this option to false if you want to move the Publisher's
* video element (or its object element in Internet Explorer) in
* the HTML DOM.
*
* If you set this to false, do not set the targetElement
* parameter. (This results in an error passed into to the OT.initPublisher()
* callback function.) To add the video to the HTML DOM, add an event listener for the
* videoElementCreated event, and then add the element property of
* the event object into the HTML DOM.
*
insertMode (String) Specifies how the Subscriber object will
* be inserted in the HTML DOM. See the targetElement parameter. This
* string can have the following values:
* *
"replace" The Subscriber object replaces contents of the
* targetElement. This is the default."after" The Subscriber object is a new element inserted
* after the targetElement in the HTML DOM. (Both the Subscriber and targetElement
* have the same parent element.)"before" The Subscriber object is a new element inserted
* before the targetElement in the HTML DOM. (Both the Subsciber and targetElement
* have the same parent element.)"append" The Subscriber object is a new element added as a
* child of the targetElement. If there are other child elements, the Subscriber is
* appended as the last child element of the targetElement. Do not move the publisher element or its parent elements in the DOM
* heirarchy. Use CSS to resize or reposition the publisher video's element
* (the element property of the Publisher object) or its parent element (see
*
* Resizing or repositioning a video).
preferredFrameRate (Number) The preferred frame rate of the subscriber's
* video. This method only applies to a video that was published using the scalable video beta
* feature. Lowering the preferred frame rate lowers video quality on the subscribing client,
* but it also reduces network and CPU usage. You may want to use a lower frame rate for
* subscribers to a stream that is less important than other streams.
*
* Not every frame rate is available to a subscriber. When you set the preferred frame rate for
* the subscriber, OpenTok.js picks the best frame rate available that matches your setting.
* The frame rates available are based on the value of the Subscriber object's
* stream.frameRate property, which represents the maximum value available for the
* stream. The actual frame rates available depend, dynamically, on network and CPU resources
* available to the publisher and subscriber.
*
* You can dynamically change the preferred frame rate used by calling the
* setPreferredFrameRate() method of the Subscriber object.
*
* To participate in the scalable video beta program, see * the OpenTok Beta programs page. *
preferredResolution (Object) The preferred resolution of the subscriber's
* video. This method only applies to a video that was published using the scalable video beta
* feature. Set this to an object with two properties: width and height
* (both numbers), such as {width: 320, height: 240}. Lowering the preferred video
* resolution lowers video quality on the subscribing client, but it also reduces network and CPU
* usage. You may want to use a lower resolution based on the dimensions of subscriber's video on
* the web page. You may want to use a resolution for subscribers to a stream that is less
* important (and smaller) than other streams.
*
* Not every resolution is available to a subscriber. When you set the preferred resolution,
* OpenTok.js and the video encoder pick the best resolution available that matches your
* setting. The resolutions available depend on the resolution of the published stream.
* The Subscriber object's stream.resolution property represents the highest
* resolution available for the stream. Each of the resolutions available for a stream will use
* the same aspect ratio. The actual resolutions available depend, dynamically, on network
* and CPU resources available to the publisher and subscriber.
*
* You can dynamically change the preferred video resolution used by calling the
* setPreferredResolution() method of the Subscriber object.
*
* To participate in the scalable video beta program, see the * OpenTok Beta programs page. *
showControls (Boolean) Whether to display the built-in user interface
* controls for the Subscriber (default: true). These controls include the name
* display, the audio level indicator, the speaker control button, the video disabled indicator,
* and the video disabled warning icon. You can turn off all user interface controls by setting
* this property to false. You can control the display of individual user interface
* controls by leaving this property set to true (the default) and setting
* individual properties of the style property.
* style (Object) An object containing properties that define the initial
* appearance of user interface controls of the Subscriber. The style object
* includes the following properties:
* audioLevelDisplayMode (String) — How to display the audio level
* indicator. Possible values are: "auto" (the indicator is displayed when the
* video is disabled), "off" (the indicator is not displayed), and
* "on" (the indicator is always displayed).backgroundImageURI (String) — A URI for an image to display as
* the background image when a video is not displayed. (A video may not be displayed if
* you call subscribeToVideo(false) on the Subscriber object). You can pass an
* http or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the
* data URI scheme (instead of http or https) and pass in base-64-encrypted
* PNG data, such as that obtained from the
* Subscriber.getImgData() method. For example,
* you could set the property to "data:VBORw0KGgoAA...", where the portion of
* the string after "data:" is the result of a call to
* Subscriber.getImgData(). If the URL or the image data is invalid, the
* property is ignored (the attempt to set the image fails silently).buttonDisplayMode (String) — How to display the speaker controls
* Possible values are: "auto" (controls are displayed when the stream is first
* displayed and when the user mouses over the display), "off" (controls are not
* displayed), and "on" (controls are always displayed).nameDisplayMode (String) Whether to display the stream name.
* Possible values are: "auto" (the name is displayed when the stream is first
* displayed and when the user mouses over the display), "off" (the name is not
* displayed), and "on" (the name is always displayed).videoDisabledDisplayMode (String) Whether to display the video
* disabled indicator and video disabled warning icons for a Subscriber. These icons
* indicate that the video has been disabled (or is in risk of being disabled for
* the warning icon) due to poor stream quality. This style only applies to the Subscriber
* object. Possible values are: "auto" (the icons are automatically when the
* displayed video is disabled or in risk of being disabled due to poor stream quality),
* "off" (do not display the icons), and "on" (display the
* icons). The default setting is "auto"subscribeToAudio (Boolean) Whether to initially subscribe to audio
* (if available) for the stream (default: true).subscribeToVideo (Boolean) Whether to initially subscribe to video
* (if available) for the stream (default: true).testNetwork (Boolean) Whether, when subscribing to a stream
* published by the local client, you want to have the stream come from the OpenTok Media
* Router (true) or if you want the DOM to simply to display the local camera's
* video (false). Set this to true when you want to use the
* Subscriber.getStats() method to check statistics
* for a stream you publish. This setting only applies to streams published by the local
* client in a session that uses the OpenTok Media Router (sessions with the
* media mode
* set to routed), not in sessions with the media mode set to relayed. The default value is
* false.width (Number) The desired initial width of the displayed
* video in the HTML page (default: 264 pixels). You can specify the number of pixels as
* either a number (such as 400) or a string ending in "px" (such as "400px"). Or you can
* specify a percentage of the size of the parent element, with a string ending in "%"
* (such as "100%"). Note: To resize the video, adjust the CSS of the subscriber's
* DOM element (the element property of the Subscriber object) or (if the
* width is specified as a percentage) its parent DOM element (see
*
* Resizing or repositioning a video).
* subscribe() method succeeds or fails. This function takes one parameter —
* error. On success, the completionHandler function is not passed any
* arguments. On error, the function is passed an error object, defined by the
* Error class, has two properties: code (an integer) and
* message (a string), which identify the cause of the failure. The following
* code adds a completionHandler when calling the subscribe() method:
*
* session.subscribe(stream, "subscriber", null, function (error) {
* if (error) {
* console.log(error.message);
* } else {
* console.log("Subscribed to stream: " + stream.id);
* }
* });
*
*
* @signature subscribe(stream, targetElement, properties, completionHandler)
* @returns {Subscriber} The Subscriber object for this stream. Stream control functions
* are exposed through the Subscriber object.
* @method #subscribe
* @memberOf Session
*/
this.subscribe = function(stream, targetElement, properties, completionHandler) {
if (typeof targetElement === 'function') {
completionHandler = targetElement;
targetElement = undefined;
properties = undefined;
}
if (typeof properties === 'function') {
completionHandler = properties;
properties = undefined;
}
if (!this.connection || !this.connection.connectionId) {
dispatchError(ExceptionCodes.UNABLE_TO_SUBSCRIBE,
'Session.subscribe :: Connection required to subscribe',
completionHandler);
return undefined;
}
if (!stream) {
dispatchError(ExceptionCodes.UNABLE_TO_SUBSCRIBE,
'Session.subscribe :: stream cannot be null',
completionHandler);
return undefined;
}
if (!stream.hasOwnProperty('streamId')) {
dispatchError(ExceptionCodes.UNABLE_TO_SUBSCRIBE,
'Session.subscribe :: invalid stream object',
completionHandler);
return undefined;
}
if (properties && properties.insertDefaultUI === false && targetElement) {
dispatchError(ExceptionCodes.INVALID_PARAMETER,
'You cannot specify a target element if insertDefaultUI is false',
completionHandler);
return undefined;
}
if (targetElement && targetElement.insertDefaultUI === false) {
// You can omit the targetElement property if you set insertDefaultUI to false
properties = targetElement;
targetElement = undefined;
}
var subscriber = new Subscriber(targetElement, OTHelpers.extend(properties || {}, {
stream: stream,
session: this
}), function(err) {
if (err) {
var errorCode, errorMessage;
var knownErrorCodes = [400, 403];
if (!err.code && knownErrorCodes.indexOf(err.code) > -1) {
errorCode = ExceptionCodes.UNABLE_TO_SUBSCRIBE; // TODO: this is untested
errorMessage = 'Session.subscribe :: ' + err.message;
} else {
errorCode = ExceptionCodes.UNEXPECTED_SERVER_RESPONSE;
errorMessage = 'Unexpected server response. Try this operation again later.';
}
dispatchError(errorCode, errorMessage, completionHandler);
} else if (completionHandler && OTHelpers.isFunction(completionHandler)) {
completionHandler.apply(null, arguments);
}
});
sessionObjects.subscribers.add(subscriber);
return subscriber;
};
/**
* Stops subscribing to a stream in the session. the display of the audio-video stream is
* removed from the local web page.
*
* * The following code subscribes to other clients' streams. For each stream, the code also * adds an Unsubscribe link. *
*
* var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
* var sessionID = ""; // Replace with your own session ID.
* // See https://dashboard.tokbox.com/projects
* var streams = [];
*
* var session = OT.initSession(apiKey, sessionID);
* session.on("streamCreated", function(event) {
* var stream = event.stream;
* displayStream(stream);
* });
* session.connect(token);
*
* function displayStream(stream) {
* var div = document.createElement('div');
* div.setAttribute('id', 'stream' + stream.streamId);
*
* var subscriber = session.subscribe(stream, div);
* subscribers.push(subscriber);
*
* var aLink = document.createElement('a');
* aLink.setAttribute('href', 'javascript: unsubscribe("' + subscriber.id + '")');
* aLink.innerHTML = "Unsubscribe";
*
* var streamsContainer = document.getElementById('streamsContainer');
* streamsContainer.appendChild(div);
* streamsContainer.appendChild(aLink);
*
* streams = event.streams;
* }
*
* function unsubscribe(subscriberId) {
* console.log("unsubscribe called");
* for (var i = 0; i < subscribers.length; i++) {
* var subscriber = subscribers[i];
* if (subscriber.id == subscriberId) {
* session.unsubscribe(subscriber);
* }
* }
* }
*
*
* @param {Subscriber} subscriber The Subscriber object to unsubcribe.
*
* @see subscribe()
*
* @method #unsubscribe
* @memberOf Session
*/
this.unsubscribe = function(subscriber) {
if (!subscriber) {
var errorMsg = 'OT.Session.unsubscribe: subscriber cannot be null';
logging.error(errorMsg);
throw new Error(errorMsg);
}
if (!subscriber.stream) {
logging.warn('OT.Session.unsubscribe:: tried to unsubscribe a subscriber that had no stream');
return false;
}
logging.debug('OT.Session.unsubscribe: subscriber ' + subscriber.id);
subscriber.destroy();
return true;
};
/**
* Returns an array of local Subscriber objects for a given stream.
*
* @param {Stream} stream The stream for which you want to find subscribers.
*
* @returns {Array} An array of {@link Subscriber} objects for the specified stream.
*
* @see unsubscribe()
* @see Subscriber
* @see StreamEvent
* @method #getSubscribersForStream
* @memberOf Session
*/
this.getSubscribersForStream = function(stream) {
return sessionObjects.subscribers.where({ streamId: stream.id });
};
/**
* Returns the local Publisher object for a given stream.
*
* @param { Stream } stream The stream for which you want to find the Publisher.
*
* @returns { Publisher } A Publisher object for the specified stream. Returns
* null if there is no local Publisher object
* for the specified stream.
*
* @see forceUnpublish()
* @see Subscriber
* @see StreamEvent
*
* @method #getPublisherForStream
* @memberOf Session
*/
this.getPublisherForStream = function(stream) {
var streamId,
errorMsg;
if (typeof stream === 'string') {
streamId = stream;
} else if (typeof stream === 'object' && stream && stream.hasOwnProperty('id')) {
streamId = stream.id;
} else {
errorMsg = 'Session.getPublisherForStream :: Invalid stream type';
logging.error(errorMsg);
throw new Error(errorMsg);
}
return sessionObjects.publishers.where({ streamId: streamId })[0];
};
// Private Session API: for internal OT use only
this._ = {
jsepCandidateP2p: function(streamId, subscriberId, candidate) {
return _socket.jsepCandidateP2p(streamId, subscriberId, candidate);
},
jsepCandidate: function(streamId, candidate) {
return _socket.jsepCandidate(streamId, candidate);
},
jsepOffer: function(streamId, offerSdp) {
return _socket.jsepOffer(streamId, offerSdp);
},
jsepAnswer: function(streamId, answerSdp) {
return _socket.jsepAnswer(streamId, answerSdp);
},
jsepAnswerP2p: function(streamId, subscriberId, answerSdp) {
return _socket.jsepAnswerP2p(streamId, subscriberId, answerSdp);
},
reconnecting: function() {
this.dispatchEvent(new Events.SessionReconnectingEvent());
}.bind(this),
reconnected: function() {
this.dispatchEvent(new Events.SessionReconnectedEvent());
if (this.sessionInfo.renegotiation) {
sessionObjects.publishers.where({ session: this }).forEach(function(publisher) {
publisher._.iceRestart();
});
sessionObjects.subscribers.where({ session: this }).forEach(function(subscriber) {
subscriber._.iceRestart();
});
}
}.bind(this),
// session.on("signal", function(SignalEvent))
// session.on("signal:{type}", function(SignalEvent))
dispatchSignal: function(fromConnection, type, data) {
var event = new Events.SignalEvent(type, data, fromConnection);
event.target = this;
// signal a "signal" event
// NOTE: trigger doesn't support defaultAction, and therefore preventDefault.
this.trigger(Events.Event.names.SIGNAL, event);
// signal an "signal:{type}" event" if there was a custom type
if (type) { this.dispatchEvent(event); }
}.bind(this),
subscriberCreate: function(stream, subscriber, channelsToSubscribeTo, completion) {
return _socket.subscriberCreate(stream.id, subscriber.widgetId,
channelsToSubscribeTo, completion);
},
subscriberDestroy: function(stream, subscriber) {
return _socket.subscriberDestroy(stream.id, subscriber.widgetId);
},
subscriberUpdate: function(stream, subscriber, attributes) {
return _socket.subscriberUpdate(stream.id, subscriber.widgetId, attributes);
},
subscriberChannelUpdate: function(stream, subscriber, channel, attributes) {
return _socket.subscriberChannelUpdate(stream.id, subscriber.widgetId, channel.id,
attributes);
},
streamCreate: function(name, streamId, audioFallbackEnabled, channels, completion) {
_socket.streamCreate(
name,
streamId,
audioFallbackEnabled,
channels,
void 0, // min bitrate config.get('bitrates', 'min', APIKEY.value),
void 0, // max bitrate config.get('bitrates', 'max', APIKEY.value),
completion
);
},
streamDestroy: function(streamId) {
_socket.streamDestroy(streamId);
},
streamChannelUpdate: function(stream, channel, attributes) {
_socket.streamChannelUpdate(stream.id, channel.id, attributes);
}
};
/**
* Sends a signal to each client or a specified client in the session. Specify a
* to property of the signal parameter to limit the signal to
* be sent to a specific client; otherwise the signal is sent to each client connected to
* the session.
* * The following example sends a signal of type "foo" with a specified data payload ("hello") * to all clients connected to the session: *
* session.signal({
* type: "foo",
* data: "hello"
* },
* function(error) {
* if (error) {
* console.log("signal error: " + error.message);
* } else {
* console.log("signal sent");
* }
* }
* );
*
*
* Calling this method without specifying a recipient client (by setting the to
* property of the signal parameter) results in multiple signals sent (one to each
* client in the session). For information on charges for signaling, see the
* OpenTok pricing page.
*
* The following example sends a signal of type "foo" with a data payload ("hello") to a * specific client connected to the session: *
* session.signal({
* type: "foo",
* to: recipientConnection; // a Connection object
* data: "hello"
* },
* function(error) {
* if (error) {
* console.log("signal error: " + error.message);
* } else {
* console.log("signal sent");
* }
* }
* );
*
*
* Add an event handler for the signal event to listen for all signals sent in
* the session. Add an event handler for the signal:type event to listen for
* signals of a specified type only (replace type, in signal:type,
* with the type of signal to listen for). The Session object dispatches these events. (See
* events.)
*
* @param {Object} signal An object that contains the following properties defining the signal:
*
data — (String) The data to send. The limit to the length of data
* string is 8kB. Do not set the data string to null or
* undefined.retryAfterReconnect— (Boolean) Upon reconnecting to the session,
* whether to send any signals that were initiated while disconnected. If your client loses its
* connection to the OpenTok session, due to a drop in network connectivity, the client
* attempts to reconnect to the session, and the Session object dispatches a
* reconnecting event. By default, signals initiated while disconnected are
* sent when (and if) the client reconnects to the OpenTok session. You can prevent this by
* setting the retryAfterReconnect property to false. (The default
* value is true.)
* to — (Connection) A Connection
* object corresponding to the client that the message is to be sent to. If you do not
* specify this property, the signal is sent to all clients connected to the session.type — (String) The type of the signal. You can use the type to
* filter signals when setting an event handler for the signal:type event
* (where you replace type with the type string). The maximum length of the
* type string is 128 characters, and it must contain only letters (A-Z and a-z),
* numbers (0-9), '-', '_', and '~'.Each property is optional. If you set none of the properties, you will send a signal * with no data or type to each client connected to the session.
* * @param {Function} completionHandler A function that is called when sending the signal * succeeds or fails. This function takes one parameter —error.
* On success, the completionHandler function is not passed any
* arguments. On error, the function is passed an error object, defined by the
* Error class. The error object has the following
* properties:
*
* code — (Number) An error code, which can be one of the following:
* | 400 | One of the signal properties is invalid. | *
| 404 | The client specified by the to property is not connected
* to the session. |
*
| 413 | The type string exceeds the maximum length (128 bytes),
* or the data string exceeds the maximum size (8 kB). |
*
| 500 | You are not connected to the OpenTok session. | *
message — (String) A description of the error.Note that the completionHandler success result (error == null)
* indicates that the options passed into the Session.signal() method are valid
* and the signal was sent. It does not indicate that the signal was successfully
* received by any of the intended recipients.
*
* @method #signal
* @memberOf Session
* @see signal and signal:type events
*/
this.signal = function(options, completion) {
var _options = options;
var _completion = completion;
if (OTHelpers.isFunction(_options)) {
_completion = _options;
_options = null;
}
if (this.isNot('connected')) {
var notConnectedErrorMsg = 'Unable to send signal - you are not connected to the session.';
dispatchError(500, notConnectedErrorMsg, _completion);
return;
}
_socket.signal(_options, _completion, this.logEvent);
if (options && options.data && typeof options.data !== 'string') {
logging.warn('Signaling of anything other than Strings is deprecated. ' +
'Please update the data property to be a string.');
}
};
/**
* Forces a remote connection to leave the session.
*
*
* The forceDisconnect() method is normally used as a moderation tool
* to remove users from an ongoing session.
*
* When a connection is terminated using the forceDisconnect(),
* sessionDisconnected, connectionDestroyed and
* streamDestroyed events are dispatched in the same way as they
* would be if the connection had terminated itself using the disconnect()
* method. However, the reason property of a {@link ConnectionEvent} or
* {@link StreamEvent} object specifies "forceDisconnected" as the reason
* for the destruction of the connection and stream(s).
*
* While you can use the forceDisconnect() method to terminate your own connection,
* calling the disconnect() method is simpler.
*
* The OT object dispatches an exception event if the user's role
* does not include permissions required to force other users to disconnect.
* You define a user's role when you create the user token using the
* generate_token() method using one of the
* OpenTok server-side libraries or
* or the Dashboard page.
* See ExceptionEvent and OT.on().
*
* The application throws an error if the session is not connected. *
* *
* connectionDestroyed (ConnectionEvent)
* On clients other than which had the connection terminated.
*
* exception (ExceptionEvent)
* The user's role does not allow forcing other user's to disconnect (event.code =
* 1530),
* or the specified stream is not publishing to the session (event.code = 1535).
*
* sessionDisconnected
* (SessionDisconnectEvent)
* On the client which has the connection terminated.
*
* streamDestroyed (StreamEvent)
* If streams are stopped as a result of the connection ending.
*
connectionId property of the Connection object).
*
* @param {Function} completionHandler (Optional) A function to be called when the call to the
* forceDiscononnect() method succeeds or fails. This function takes one parameter
* — error. On success, the completionHandler function is
* not passed any arguments. On error, the function is passed an error object
* parameter. The error object, defined by the Error
* class, has two properties: code (an integer)
* and message (a string), which identify the cause of the failure.
* Calling forceDisconnect() fails if the role assigned to your
* token is not "moderator"; in this case error.code is set to 1520. The following
* code adds a completionHandler when calling the forceDisconnect()
* method:
*
* session.forceDisconnect(connection, function (error) {
* if (error) {
* console.log(error);
* } else {
* console.log("Connection forced to disconnect: " + connection.id);
* }
* });
*
*
* @method #forceDisconnect
* @memberOf Session
*/
this.forceDisconnect = function(connectionOrConnectionId, completionHandler) {
if (this.isNot('connected')) {
var notConnectedErrorMsg = 'Cannot call forceDisconnect(). You are not ' +
'connected to the session.';
dispatchError(ExceptionCodes.NOT_CONNECTED, notConnectedErrorMsg, completionHandler);
return;
}
var connectionId = (
typeof connectionOrConnectionId === 'string' ?
connectionOrConnectionId :
connectionOrConnectionId.id
);
var invalidParameterErrorMsg = (
'Invalid Parameter. Check that you have passed valid parameter values into the method call.'
);
if (!connectionId) {
dispatchError(
ExceptionCodes.INVALID_PARAMETER,
invalidParameterErrorMsg,
completionHandler
);
return;
}
var notPermittedErrorMsg = 'This token does not allow forceDisconnect. ' +
'The role must be at least `moderator` to enable this functionality';
if (!permittedTo('forceDisconnect')) {
// if this throws an error the handleJsException won't occur
dispatchError(
ExceptionCodes.UNABLE_TO_FORCE_DISCONNECT,
notPermittedErrorMsg,
completionHandler
);
return;
}
_socket.forceDisconnect(connectionId, function(err) {
if (err) {
dispatchError(
ExceptionCodes.INVALID_PARAMETER,
invalidParameterErrorMsg,
completionHandler
);
} else if (completionHandler && OTHelpers.isFunction(completionHandler)) {
completionHandler.apply(null, arguments);
}
});
};
/**
* Forces the publisher of the specified stream to stop publishing the stream.
*
*
* Calling this method causes the Session object to dispatch a streamDestroyed
* event on all clients that are subscribed to the stream (including the client that is
* publishing the stream). The reason property of the StreamEvent object is
* set to "forceUnpublished".
*
* The OT object dispatches an exception event if the user's role
* does not include permissions required to force other users to unpublish.
* You define a user's role when you create the user token using the generate_token()
* method using the
* OpenTok server-side libraries or the
* Dashboard page.
* You pass the token string as a parameter of the connect() method of the Session
* object. See ExceptionEvent and
* OT.on().
*
* exception (ExceptionEvent)
* The user's role does not allow forcing other users to unpublish.
*
* streamDestroyed (StreamEvent)
* The stream has been unpublished. The Session object dispatches this on all clients
* subscribed to the stream, as well as on the publisher's client.
*
forceUnpublish() method succeeds or fails. This function takes one parameter
* — error. On success, the completionHandler function is
* not passed any arguments. On error, the function is passed an error object
* parameter. The error object, defined by the Error
* class, has two properties: code (an integer)
* and message (a string), which identify the cause of the failure. Calling
* forceUnpublish() fails if the role assigned to your token is not "moderator";
* in this case error.code is set to 1530. The following code adds a
* completionHandler when calling the forceUnpublish() method:
*
* session.forceUnpublish(stream, function (error) {
* if (error) {
* console.log(error);
* } else {
* console.log("Connection forced to disconnect: " + connection.id);
* }
* });
*
*
* @method #forceUnpublish
* @memberOf Session
*/
this.forceUnpublish = function(streamOrStreamId, completionHandler) {
if (this.isNot('connected')) {
var notConnectedErrorMsg = 'Cannot call forceUnpublish(). You are not ' +
'connected to the session.';
dispatchError(ExceptionCodes.NOT_CONNECTED, notConnectedErrorMsg, completionHandler);
return;
}
var notPermittedErrorMsg = 'This token does not allow forceUnpublish. ' +
'The role must be at least `moderator` to enable this functionality';
if (permittedTo('forceUnpublish')) {
var stream = typeof streamOrStreamId === 'string' ?
this.streams.get(streamOrStreamId) : streamOrStreamId;
_socket.forceUnpublish(stream.id, function(err) {
if (err) {
dispatchError(ExceptionCodes.UNABLE_TO_FORCE_UNPUBLISH,
notPermittedErrorMsg, completionHandler);
} else if (completionHandler && OTHelpers.isFunction(completionHandler)) {
completionHandler.apply(null, arguments);
}
});
} else {
// if this throws an error the handleJsException won't occur
dispatchError(ExceptionCodes.UNABLE_TO_FORCE_UNPUBLISH,
notPermittedErrorMsg, completionHandler);
}
};
this.isConnected = function() {
return this.is('connected');
};
this.capabilities = new Capabilities([]);
/**
* Dispatched when an archive recording of the session starts.
*
* @name archiveStarted
* @event
* @memberof Session
* @see ArchiveEvent
* @see Archiving overview
*/
/**
* Dispatched when an archive recording of the session stops.
*
* @name archiveStopped
* @event
* @memberof Session
* @see ArchiveEvent
* @see Archiving overview
*/
/**
* Dispatched when a new client (including your own) has connected to the session, and for
* every client in the session when you first connect. (The Session object also dispatches
* a sessionConnected event when your local client connects.)
*
* @name connectionCreated
* @event
* @memberof Session
* @see ConnectionEvent
* @see OT.initSession()
*/
/**
* A client, other than your own, has disconnected from the session.
* @name connectionDestroyed
* @event
* @memberof Session
* @see ConnectionEvent
*/
/**
* The page has connected to an OpenTok session. This event is dispatched asynchronously
* in response to a successful call to the connect() method of a Session
* object. Before calling the connect() method, initialize the session by
* calling the OT.initSession() method. For a code example and more details,
* see Session.connect().
* @name sessionConnected
* @event
* @memberof Session
* @see SessionConnectEvent
* @see Session.connect()
* @see OT.initSession()
*/
/**
* The client has disconnected from the session. This event may be dispatched asynchronously
* in response to a successful call to the disconnect() method of the Session object.
* The event may also be disptached if a session connection is lost inadvertantly, as in the case
* of a lost network connection.
*
* The default behavior is that all Subscriber objects are unsubscribed and removed from the
* HTML DOM. Each Subscriber object dispatches a destroyed event when the element is
* removed from the HTML DOM. If you call the preventDefault() method in the event
* listener for the sessionDisconnect event, the default behavior is prevented, and
* you can, optionally, clean up Subscriber objects using your own code.
*
The reason property of the event object indicates the reason for the client
* being disconnected.
* @name sessionDisconnected
* @event
* @memberof Session
* @see Session.disconnect()
* @see Session.forceDisconnect()
* @see SessionDisconnectEvent
*/
/**
* The local client has lost its connection to an OpenTok session and is trying to reconnect.
* This results from a loss in network connectivity. If the client can reconnect to the session,
* the Session object dispatches a sessionReconnected event. Otherwise, if the client
* cannot reconnect, the Session object dispatches a sessionDisconnected event.
*
* Automatic reconnection is a beta feature. To participate in the beta program, see the * OpenTok Beta programs page. *
* In response to this event, you may want to provide a user interface notification, to let
* the user know that the app is trying to reconnect to the session and that audio-video streams
* are temporarily disconnected.
*
* @name sessionReconnecting
* @event
* @memberof Session
* @see Event
* @see sessionReconnected event
* @see sessionDisconnected event
*/
/**
* The local client has reconnected to the OpenTok session after its connection was lost
* temporarily. When the connection is lost, the Session object dispatches a
* sessionReconnecting event, prior to the sessionReconnected
* event. If the client cannot reconnect to the session, the Session object dispatches a
* sessionDisconnected event instead of this event.
*
* Automatic reconnection is a beta feature. To participate in the beta program, see the * OpenTok Beta programs page. *
* Any existing publishers and subscribers are automatically reconnected when client reconnects * and the Session object dispatches this event. *
* Any signals sent by other clients while your client was disconnected are received upon
* reconnecting. By default, signals initiated by the the local client while disconnected
* (by calling the Session.signal() method) are sent when the client reconnects
* to the OpenTok session. You can prevent this by setting the retryAfterReconnect
* property to false in the signal object you pass into the
* Session.signal() method.
*
* @name sessionReconnected
* @event
* @memberof Session
* @see Event
* @see sessionReconnecting event
* @see sessionDisconnected event
*/
/**
* A new stream, published by another client, has been created on this session. For streams
* published by your own client, the Publisher object dispatches a streamCreated
* event. For a code example and more details, see {@link StreamEvent}.
* @name streamCreated
* @event
* @memberof Session
* @see StreamEvent
* @see Session.publish()
*/
/**
* A stream from another client has stopped publishing to the session.
*
* The default behavior is that all Subscriber objects that are subscribed to the stream are
* unsubscribed and removed from the HTML DOM. Each Subscriber object dispatches a
* destroyed event when the element is removed from the HTML DOM. If you call the
* preventDefault() method in the event listener for the
* streamDestroyed event, the default behavior is prevented and you can clean up
* Subscriber objects using your own code. See
* Session.getSubscribersForStream().
*
* For streams published by your own client, the Publisher object dispatches a
* streamDestroyed event.
*
* For a code example and more details, see {@link StreamEvent}. * @name streamDestroyed * @event * @memberof Session * @see StreamEvent */ /** * Defines an event dispatched when property of a stream has changed. This can happen in * in the following conditions: *
*
videoDisabled and videoEnabled events in all
* conditions that cause the subscriber's stream to be disabled or enabled.
* videoDimensions property of the Stream object has
* changed (see Stream.videoDimensions).
* videoType property of the Stream object has changed.
* This can happen in a stream published by a mobile device. (See
* Stream.videoType.)
* data — (String) The data string sent with the signal (if there
* is one).from — (Connection) The Connection
* corresponding to the client that sent with the signal.type — (String) The type assigned to the signal (if there is
* one).
* You can register to receive all signals sent in the session, by adding an event handler
* for the signal event. For example, the following code adds an event handler
* to process all signals sent in the session:
*
* session.on("signal", function(event) {
* console.log("Signal sent from connection: " + event.from.id);
* console.log("Signal data: " + event.data);
* });
*
* You can register for signals of a specfied type by adding an event handler for the
* signal:type event (replacing type with the actual type string
* to filter on).
*
* @name signal
* @event
* @memberof Session
* @see Session.signal()
* @see SignalEvent
* @see signal:type event
*/
/**
* A signal of the specified type was received from the session. The
* SignalEvent class defines this event object.
* It includes the following properties:
*
data — (String) The data string sent with the signal.from — (Connection) The Connection
* corresponding to the client that sent with the signal.type — (String) The type assigned to the signal (if there is one).
*
* You can register for signals of a specfied type by adding an event handler for the
* signal:type event (replacing type with the actual type string
* to filter on). For example, the following code adds an event handler for signals of
* type "foo":
*
* session.on("signal:foo", function(event) {
* console.log("foo signal sent from connection " + event.from.id);
* console.log("Signal data: " + event.data);
* });
*
*
* You can register to receive all signals sent in the session, by adding an event
* handler for the signal event.
*
* @name signal:type
* @event
* @memberof Session
* @see Session.signal()
* @see SignalEvent
* @see signal event
*/
};
SessionHandle.Session.RaptorSocket = RaptorSocket;
},{"../../helpers/connectivity_attempt_pinger.js":145,"../../helpers/properties.js":154,"../analytics.js":166,"../api_key.js":168,"../capabilities.js":171,"../events.js":182,"../exception_codes.js":183,"../logging.js":187,"../messaging/raptor/raptor_socket.js":195,"../messaging/raptor/session_dispatcher.js":197,"../ot_error.js":208,"../publisher":224,"../publisher/init.js":225,"../qos_testing/http_test.js":229,"../qos_testing/webrtc_test.js":230,"../subscriber":247,"../system_requirements.js":249,"./info.js":237,"./objects.js":239,"./tag.js":240,"@opentok/ot-helpers":4,"@opentok/otplugin.js":40,"bluebird":70,"lodash.omit":101,"uuid":137}],237:[function(require,module,exports){
'use strict';
var Anvil = require('../anvil.js');
var Bluebird = require('bluebird');
var ExceptionCodes = require('../exception_codes.js');
var logging = require('../logging.js');
// This sequence defines the delay before retry. Therefore a 0 indicates
// that a retry should happen immediately.
//
// e.g. 0, 600, 1200 means retry immediately, then in 600 ms, then in 1200ms
//
var retryDelays = [0, 600, 1200];
// These codes are possibly transient and it's worth retrying if a Anvil request
// fails with one of these codes.
var transientErrorCodes = [
ExceptionCodes.CONNECT_FAILED,
ExceptionCodes.UNEXPECTED_SERVER_RESPONSE
];
var SessionInfo = function(rawSessionInfo) {
logging.log('SessionInfo Response:');
logging.log(rawSessionInfo);
/*jshint camelcase:false*/
//jscs:disable requireCamelCaseOrUpperCaseIdentifiers
this.sessionId = rawSessionInfo.session_id;
this.partnerId = rawSessionInfo.partner_id;
this.messagingServer = rawSessionInfo.messaging_server_url;
this.mediaServerName = rawSessionInfo.media_server_hostname;
this.messagingURL = rawSessionInfo.messaging_url;
this.symphonyAddress = rawSessionInfo.symphony_address;
if (rawSessionInfo.properties) {
// `simulcast` is tri-state:
// true: simulcast is on for this session
// false: simulcast is off for this session
// undefined: the developer can choose
//
this.simulcast = rawSessionInfo.properties.simulcast;
this.reconnection = rawSessionInfo.properties.reconnection === undefined ?
false : rawSessionInfo.properties.reconnection;
this.renegotiation = rawSessionInfo.properties.renegotiation === undefined ?
false : rawSessionInfo.properties.renegotiation;
this.p2pEnabled = !!(rawSessionInfo.properties.p2p &&
rawSessionInfo.properties.p2p.preference &&
rawSessionInfo.properties.p2p.preference.value === 'enabled');
this.rememberDeviceChoiceIE = rawSessionInfo.properties.rememberDeviceChoiceIE === undefined ?
false : rawSessionInfo.properties.rememberDeviceChoiceIE;
} else {
this.p2pEnabled = false;
}
};
// Retrieves Session Info for +session+. The SessionInfo object will be passed
// to the +onSuccess+ callback. The +onFailure+ callback will be passed an error
// object and the DOMEvent that relates to the error.
//
SessionInfo.get = function(id, token) {
var remainingRetryDelays = retryDelays.slice();
var attempt = function(err, resolve, reject) {
if (remainingRetryDelays.length === 0) {
reject(err);
return;
}
Anvil.get('session/' + id + '?extended=true', token).then(function(anvilResponse) {
resolve(new SessionInfo(anvilResponse));
}, function(err) {
if (transientErrorCodes.indexOf(err.code) > -1) {
// This error is possibly transient, so retry
setTimeout(function() {
attempt(err, resolve, reject);
}, remainingRetryDelays.shift());
} else {
reject(err);
}
});
};
return new Bluebird.Promise(function(resolve, reject) {
attempt(void 0, resolve, reject);
});
};
module.exports = SessionInfo;
},{"../anvil.js":167,"../exception_codes.js":183,"../logging.js":187,"bluebird":70}],238:[function(require,module,exports){
'use strict';
var SessionHandle = require('./handle.js');
var sessionObjects = require('./objects.js');
/**
* The first step in using the OpenTok API is to call the OT.initSession()
* method. Other methods of the OT object check for system requirements and set up error logging.
*
* @class OT
*/
/**
*
* Initializes and returns the local session object for a specified session ID. *
*
* You connect to an OpenTok session using the connect() method
* of the Session object returned by the OT.initSession() method.
* Note that calling OT.initSession() does not initiate communications
* with the cloud. It simply initializes the Session object that you can use to
* connect (and to perform other operations once connected).
*
* Note that you can initialize only one Session object per session ID. Calling
* OT.initSession() a second time with the same session ID returns the
* previously created Session object. However, you can initialize multiple Session objects
* for different sessions (with different session IDs) to connect to multiple sessions.
*
* When the client disconnects from the session (and the session dispatches a
* sessionDisconnected event), you can reuse the Session object to
* reconnect to the session. However, the Session object will retain any existing
* event handlers attached to it. You can remove all event handlers by calling the
* off() method of the Session object (passing in no parameters).
*
* For an example, see Session.connect(). *
* * @method OT.initSession * @memberof OT * @param {String} apiKey Your OpenTok API key (see the * OpenTok dashboard). * @param {String} sessionId The session ID identifying the OpenTok session. For more * information, see Session * creation. * @returns {Session} The session object through which all further interactions with * the session will occur. */ module.exports = function initSession(apiKey, sessionId) { if (sessionId == null) { sessionId = apiKey; apiKey = null; } // Allow buggy legacy behavior to succeed, where the client can connect if sessionId // is an array containing one element (the session ID), but fix it so that sessionId // is stored as a string (not an array): if (Array.isArray(sessionId) && sessionId.length === 1) { sessionId = sessionId[0]; } var session = sessionObjects.sessions.get(sessionId); if (!session) { session = new SessionHandle.Session(apiKey, sessionId); sessionObjects.sessions.add(session); } return session; }; },{"./handle.js":236,"./objects.js":239}],239:[function(require,module,exports){ 'use strict'; var OTHelpers = require('@opentok/ot-helpers'); var sessionObjects = {}; // TODO: Eliminate the need for this module, which is globally tracking these objects. // Publishers are id'd by their guid sessionObjects.publishers = new OTHelpers.Collection('guid'); // Subscribers are id'd by their widgetId sessionObjects.subscribers = new OTHelpers.Collection('widgetId'); sessionObjects.sessions = new OTHelpers.Collection(); module.exports = sessionObjects; },{"@opentok/ot-helpers":4}],240:[function(require,module,exports){ 'use strict'; // This is used to break the dependency Raptor had on Session. It only needs to be able to know // whether an object is an instanceof a Session. The dependency was an issue for node because // Session depends on get_user_media.js which doesn't work in node. module.exports = {}; },{}],241:[function(require,module,exports){ 'use strict'; var Events = require('./events.js'); var logging = require('./logging.js'); var OTHelpers = require('@opentok/ot-helpers'); var sessionObjects = require('./session/objects.js'); var validPropertyNames = ['name', 'archiving']; /** * Specifies a stream. A stream is a representation of a published stream in a session. When a * client calls the Session.publish() method, a new stream is * created. Properties of the Stream object provide information about the stream. * *When a stream is added to a session, the Session object dispatches a
* streamCreatedEvent. When a stream is destroyed, the Session object dispatches a
* streamDestroyed event. The StreamEvent object, which defines these event objects,
* has a stream property, which is an array of Stream object. For details and a code
* example, see {@link StreamEvent}.
When a connection to a session is made, the Session object dispatches a
* sessionConnected event, defined by the SessionConnectEvent object. The
* SessionConnectEvent object has a streams property, which is an array of Stream
* objects pertaining to the streams in the session at that time. For details and a code example,
* see {@link SessionConnectEvent}.
connection property of the Session object to see if the stream is being published
* by the local web page.
*
* @property {Number} creationTime The timestamp for the creation
* of the stream. This value is calculated in milliseconds. You can convert this value to a
* Date object by calling new Date(creationTime), where creationTime is
* the creationTime property of the Stream object.
*
* @property {Number} frameRate The frame rate of the video stream. This property is only set if
* the publisher of the stream specifies a frame rate when calling the
* OT.initPublisher() method; otherwise, this property is undefined.
*
* @property {Boolean} hasAudio Whether the stream has audio. This property can change if the
* publisher turns on or off audio (by calling
* Publisher.publishAudio()). When this occurs, the
* {@link Session} object dispatches a streamPropertyChanged event (see
* {@link StreamPropertyChangedEvent}).
*
* @property {Boolean} hasVideo Whether the stream has video. This property can change if the
* publisher turns on or off video (by calling
* Publisher.publishVideo()). When this occurs, the
* {@link Session} object dispatches a streamPropertyChanged event (see
* {@link StreamPropertyChangedEvent}).
*
* @property {String} name The name of the stream. Publishers can specify a name when publishing
* a stream (using the publish() method of the publisher's Session object).
*
* @property {String} streamId The unique ID of the stream.
*
* @property {Object} videoDimensions This object has two properties: width and
* height. Both are numbers. The width property is the width of the
* encoded stream; the height property is the height of the encoded stream. (These
* are independent of the actual width of Publisher and Subscriber objects corresponding to the
* stream.) This property can change if a stream published from a mobile device resizes, based on
* a change in the device orientation. When the video dimensions change,
* the {@link Session} object dispatches a streamPropertyChanged event
* (see {@link StreamPropertyChangedEvent}).
*
* @property {String} videoType The type of video — either "camera" or
* "screen". A "screen" video uses screen sharing on the publisher
* as the video source; for other videos, this property is set to "camera".
* This property can change if a stream published from a mobile device changes from a
* camera to a screen-sharing video type. When the video type changes, the {@link Session} object
* dispatches a streamPropertyChanged event (see {@link StreamPropertyChangedEvent}).
*/
module.exports = function Stream(id, name, creationTime, connection, session, channel) {
var self = this;
var destroyedReason;
this.id = this.streamId = id;
this.name = name;
this.creationTime = Number(creationTime);
this.connection = connection;
this.channel = channel;
this.publisher = sessionObjects.publishers.find({ streamId: this.id });
OTHelpers.eventing(this);
var onChannelUpdate = function(channel, key, oldValue, newValue) {
var _key = key;
switch (_key) {
case 'active':
_key = channel.type === 'audio' ? 'hasAudio' : 'hasVideo';
self[_key] = newValue;
break;
case 'disableWarning':
_key = channel.type === 'audio' ? 'audioDisableWarning' : 'videoDisableWarning';
self[_key] = newValue;
if (!self[channel.type === 'audio' ? 'hasAudio' : 'hasVideo']) {
return; // Do NOT event in this case.
}
break;
case 'fitMode':
_key = 'defaultFitMode';
self[_key] = newValue;
break;
case 'source':
_key = channel.type === 'audio' ? 'audioType' : 'videoType';
self[_key] = newValue;
break;
case 'videoDimensions':
self.videoDimensions = newValue;
break;
case 'orientation':
case 'width':
case 'height':
// We dispatch this via the videoDimensions key instead so do not
// trigger an event for them.
return;
default:
}
self.dispatchEvent(new Events.StreamUpdatedEvent(self, _key, oldValue, newValue));
};
var associatedWidget = function() {
if (self.publisher) {
return self.publisher;
}
return sessionObjects.subscribers.find(function(subscriber) {
return subscriber.stream && subscriber.stream.id === self.id &&
subscriber.session.id === session.id;
});
};
// Returns true if this stream is subscribe to.
var isBeingSubscribedTo = function() {
// @fixme This is not strictly speaking the right test as a stream
// can be published and subscribed by the same connection. But the
// update features don't handle this case properly right now anyway.
//
// The issue is that the stream needs to know whether the stream is
// 'owned' by a publisher or a subscriber. The reason for that is that
// when a Publisher updates a stream channel then we need to send the
// `streamChannelUpdate` message, whereas if a Subscriber does then we
// need to send `subscriberChannelUpdate`. The current code will always
// send `streamChannelUpdate`.
return !self.publisher;
};
// Returns all channels that have a type of +type+.
this.getChannelsOfType = function(type) {
return self.channel.filter(function(channel) {
return channel.type === type;
});
};
this.getChannel = function(id) {
for (var i = 0; i < self.channel.length; ++i) {
if (self.channel[i].id === id) { return self.channel[i]; }
}
return null;
};
//// implement the following using the channels
// hasAudio
// hasVideo
// videoDimensions
var audioChannel = this.getChannelsOfType('audio')[0];
var videoChannel = this.getChannelsOfType('video')[0];
// @todo this should really be: "has at least one video/audio track" instead of
// "the first video/audio track"
this.hasAudio = audioChannel != null && audioChannel.active;
this.hasVideo = videoChannel != null && videoChannel.active;
this.videoType = videoChannel && videoChannel.source;
this.defaultFitMode = videoChannel && videoChannel.fitMode;
this.videoDimensions = {};
if (videoChannel) {
this.videoDimensions.width = videoChannel.width;
this.videoDimensions.height = videoChannel.height;
this.videoDimensions.orientation = videoChannel.orientation;
videoChannel.on('update', onChannelUpdate);
this.frameRate = videoChannel.frameRate;
}
if (audioChannel) {
audioChannel.on('update', onChannelUpdate);
}
this.setChannelActiveState = function(channelType, activeState, activeReason) {
var attributes = {
active: activeState
};
if (activeReason) {
attributes.activeReason = activeReason;
}
updateChannelsOfType(channelType, attributes);
};
this.setVideoDimensions = function(width, height) {
updateChannelsOfType('video', {
width: width,
height: height,
orientation: 0
});
};
this.setRestrictFrameRate = function(restrict) {
updateChannelsOfType('video', {
restrictFrameRate: restrict
});
};
this.setPreferredResolution = function(resolution) {
if (!isBeingSubscribedTo()) {
logging.warn('setPreferredResolution has no affect when called by a publisher');
return;
}
if (session.sessionInfo.p2pEnabled) {
logging.warn('Stream.setPreferredResolution will not work in a P2P Session');
return;
}
if (resolution &&
resolution.width === void 0 &&
resolution.height === void 0) {
return;
}
// This duplicates some of the code in updateChannelsOfType. We do this for a
// couple of reasons:
// 1. Because most of the work that updateChannelsOfType does is in calling
// getChannelsOfType, which we need to do here anyway so that we can update
// the value of maxResolution in the Video Channel.
// 2. updateChannelsOfType on only sends a message to update the channel in
// Rumor. The client then expects to receive a subsequent channel update
// indicating that the update was successful. We don't receive those updates
// for preferredFrameRate/maxResolution so we need to complete both tasks and it's
// neater to do the related tasks right next to each other.
// 3. This code shouldn't be in Stream anyway. There is way too much coupling
// between Stream, Session, Publisher, and Subscriber. This will eventually be
// fixed, and when it is then it will be easier to exact the code if it's a
// single piece.
//
var video = self.getChannelsOfType('video')[0];
if (!video) {
return;
}
if (resolution && resolution.width) {
if (isNaN(parseInt(resolution.width, 10))) {
throw new OTHelpers.Error('stream preferred width must be an integer', 'Subscriber');
}
video.preferredWidth = parseInt(resolution.width, 10);
} else {
video.preferredWidth = void 0;
}
if (resolution && resolution.height) {
if (isNaN(parseInt(resolution.height, 10))) {
throw new OTHelpers.Error('stream preferred height must be an integer', 'Subscriber');
}
video.preferredHeight = parseInt(resolution.height, 10);
} else {
video.preferredHeight = void 0;
}
session._.subscriberChannelUpdate(self, associatedWidget(), video, {
preferredWidth: video.preferredWidth || 0,
preferredHeight: video.preferredHeight || 0
});
};
this.getPreferredResolution = function() {
var videoChannel = self.getChannelsOfType('video')[0];
if (!videoChannel || (!videoChannel.preferredWidth && !videoChannel.preferredHeight)) {
return void 0;
}
return {
width: videoChannel.preferredWidth,
height: videoChannel.preferredHeight
};
};
this.setPreferredFrameRate = function(preferredFrameRate) {
if (!isBeingSubscribedTo()) {
logging.warn('setPreferredFrameRate has no affect when called by a publisher');
return;
}
if (session.sessionInfo.p2pEnabled) {
logging.warn('Stream.setPreferredFrameRate will not work in a P2P Session');
return;
}
if (preferredFrameRate && isNaN(parseFloat(preferredFrameRate))) {
throw new OTHelpers.Error('stream preferred frameRate must be a number', 'Subscriber');
}
// This duplicates some of the code in updateChannelsOfType. We do this for a
// couple of reasons:
// 1. Because most of the work that updateChannelsOfType does is in calling
// getChannelsOfType, which we need to do here anyway so that we can update
// the value of preferredFrameRate in the Video Channel.
// 2. updateChannelsOfType on only sends a message to update the channel in
// Rumor. The client then expects to receive a subsequent channel update
// indicating that the update was successful. We don't receive those updates
// for preferredFrameRate/maxResolution so we need to complete both tasks and it's
// neater to do the related tasks right next to each other.
// 3. This code shouldn't be in Stream anyway. There is way too much coupling
// between Stream, Session, Publisher, and Subscriber. This will eventually be
// fixed, and when it is then it will be easier to exact the code if it's a
// single piece.
//
var video = self.getChannelsOfType('video')[0];
if (video) {
video.preferredFrameRate = preferredFrameRate ? parseFloat(preferredFrameRate) : void 0;
session._.subscriberChannelUpdate(self, associatedWidget(), video, {
preferredFrameRate: video.preferredFrameRate || 0
});
}
};
this.getPreferredFrameRate = function() {
var videoChannel = self.getChannelsOfType('video')[0];
return videoChannel ? videoChannel.preferredFrameRate : void 0;
};
var updateChannelsOfType = function(channelType, attributes) {
var setChannelActiveState;
if (!self.publisher) {
var subscriber = associatedWidget();
setChannelActiveState = function(channel) {
session._.subscriberChannelUpdate(self, subscriber, channel, attributes);
};
} else {
setChannelActiveState = function(channel) {
session._.streamChannelUpdate(self, channel, attributes);
};
}
self.getChannelsOfType(channelType).forEach(setChannelActiveState);
};
this.destroyed = false;
this.destroyedReason = void 0;
this.destroy = function(reason, quiet) {
destroyedReason = reason || 'clientDisconnected';
self.destroyed = true;
self.destroyedReason = destroyedReason;
if (quiet !== true) {
self.dispatchEvent(
new Events.DestroyedEvent(
'destroyed', // This should be Events.Event.names.STREAM_DESTROYED, but
// the value of that is currently shared with Session
self,
destroyedReason
)
);
}
};
/// PRIVATE STUFF CALLED BY Raptor.Dispatcher
// Confusingly, this should not be called when you want to change
// the stream properties. This is used by Raptor dispatch to notify
// the stream that it's properies have been successfully updated
//
// @todo make this sane. Perhaps use setters for the properties that can
// send the appropriate Raptor message. This would require that Streams
// have access to their session.
//
this._ = {};
this._.updateProperty = function(key, value) {
if (validPropertyNames.indexOf(key) === -1) {
logging.warn('Unknown stream property "' + key + '" was modified to "' + value + '".');
return;
}
var oldValue = self[key];
var newValue = value;
switch (key) {
case 'name':
self[key] = newValue;
break;
case 'archiving':
var widget = associatedWidget();
if (self.publisher && widget) {
widget._.archivingStatus(newValue);
}
self[key] = newValue;
break;
default:
}
var event = new Events.StreamUpdatedEvent(self, key, oldValue, newValue);
self.dispatchEvent(event);
};
// Mass update, called by Raptor.Dispatcher
this._.update = function(attributes) {
for (var key in attributes) {
if (!attributes.hasOwnProperty(key)) {
continue;
}
self._.updateProperty(key, attributes[key]);
}
};
this._.updateChannel = function(channelId, attributes) {
self.getChannel(channelId).update(attributes);
};
};
},{"./events.js":182,"./logging.js":187,"./session/objects.js":239,"@opentok/ot-helpers":4}],242:[function(require,module,exports){
'use strict';
var OTHelpers = require('@opentok/ot-helpers');
var logging = require('./logging.js');
var VideoOrientation = require('../helpers/video_orientation.js');
// id: String | mandatory | immutable
// type: String {video/audio/data/...} | mandatory | immutable
// active: Boolean | mandatory | mutable
// orientation: Integer? | optional | mutable
// frameRate: Float | optional | mutable
// height: Integer | optional | mutable
// width: Integer | optional | mutable
// preferredFrameRate: Float | optional | mutable
// preferredHeight: Integer | optional | mutable
// preferredWidth: Integer | optional | mutable
//
module.exports = function StreamChannel(options) {
this.id = options.id;
this.type = options.type;
this.active = OTHelpers.castToBoolean(options.active);
this.orientation = options.orientation || VideoOrientation.ROTATED_NORMAL;
if (options.frameRate) { this.frameRate = parseFloat(options.frameRate); }
if (options.preferredFrameRate) { this.preferredFrameRate = parseFloat(options.preferredFrameRate); }
if (options.preferredWidth) { this.preferredWidth = parseInt(options.preferredWidth, 10); }
if (options.preferredHeight) { this.preferredHeight = parseInt(options.preferredHeight, 10); }
this.width = parseInt(options.width, 10);
this.height = parseInt(options.height, 10);
// The defaults are used for incoming streams from pre 2015Q1 release clients.
this.source = options.source || 'camera';
this.fitMode = options.fitMode || 'cover';
OTHelpers.eventing(this, true);
// Returns true if a property was updated.
this.update = function(attributes) {
var videoDimensions = {};
var oldVideoDimensions = {};
for (var key in attributes) {
if (!attributes.hasOwnProperty(key)) {
continue;
}
// we shouldn't really read this before we know the key is valid
var oldValue = this[key];
switch (key) {
case 'active':
this.active = OTHelpers.castToBoolean(attributes[key]);
break;
case 'disableWarning':
this.disableWarning = OTHelpers.castToBoolean(attributes[key]);
break;
case 'frameRate':
this.frameRate = parseFloat(attributes[key], 10);
break;
case 'width':
case 'height':
this[key] = parseInt(attributes[key], 10);
videoDimensions[key] = this[key];
oldVideoDimensions[key] = oldValue;
break;
case 'orientation':
this[key] = attributes[key];
videoDimensions[key] = this[key];
oldVideoDimensions[key] = oldValue;
break;
case 'fitMode':
this[key] = attributes[key];
break;
case 'source':
this[key] = attributes[key];
break;
default:
logging.warn('Tried to update unknown key ' + key + ' on ' + this.type +
' channel ' + this.id);
return false;
}
this.trigger('update', this, key, oldValue, this[key]);
}
if (Object.keys(videoDimensions).length) {
// To make things easier for the public API, we broadcast videoDimensions changes,
// which is an aggregate of width, height, and orientation changes.
this.trigger('update', this, 'videoDimensions', oldVideoDimensions, videoDimensions);
}
return true;
};
};
},{"../helpers/video_orientation.js":163,"./logging.js":187,"@opentok/ot-helpers":4}],243:[function(require,module,exports){
'use strict';
// When setting the background image uri of a publisher or subscriber, we allow the use of a raw
// data blob of a png, which requires the prefix 'data:image/png;base64,'. This function
// encapsulates that transformation.
module.exports = function fixBackgroundImageURI(bgImgURI) {
if (
bgImgURI.substr(0, 5) !== 'http:' &&
bgImgURI.substr(0, 6) !== 'https:' &&
bgImgURI.substr(0, 22) !== 'data:image/png;base64,'
) {
return 'data:image/png;base64,' + bgImgURI;
}
return bgImgURI;
};
},{}],244:[function(require,module,exports){
'use strict';
var fixBackgroundImageURI = require('./fix_background_image_uri.js');
var logging = require('../logging.js');
var Style = require('./style.js');
var styleHashLogFilter = require('./style_hash_log_filter.js');
/* Stylable Notes
* Some bits are controlled by multiple flags, i.e. buttonDisplayMode and nameDisplayMode.
* When there are multiple flags how is the final setting chosen?
* When some style bits are set updates will need to be pushed through to the Chrome
*/
// Mixes the StylableComponent behaviour into the +self+ object. It will
// also set the default styles to +initialStyles+.
//
// @note This Mixin is dependent on OT.Eventing.
//
//
// @example
//
// function SomeObject {
// StylableComponent(this, {
// name: 'SomeObject',
// foo: 'bar'
// });
// }
//
// var obj = new SomeObject();
// obj.getStyle('foo'); // => 'bar'
// obj.setStyle('foo', 'baz')
// obj.getStyle('foo'); // => 'baz'
// obj.getStyle(); // => {name: 'SomeObject', foo: 'baz'}
//
module.exports = function StylableComponent(
self,
initalStyles,
showControls,
logSetStyleWithPayload
) {
if (!self.trigger) {
throw new Error('OT.StylableComponent is dependent on the mixin OTHelpers.eventing. ' +
'Ensure that this is included in the object before StylableComponent.');
}
logSetStyleWithPayload = logSetStyleWithPayload || function() {};
var _readOnly = false;
// Broadcast style changes as the styleValueChanged event
var onStyleChange = function(key, value, oldValue) {
if (oldValue) {
self.trigger('styleValueChanged', key, value, oldValue);
} else {
self.trigger('styleValueChanged', key, value);
}
};
if (showControls === false) {
initalStyles = {
buttonDisplayMode: 'off',
nameDisplayMode: 'off',
audioLevelDisplayMode: 'off',
videoDisabledDisplayMode: 'off'
};
_readOnly = true;
logSetStyleWithPayload({
showControls: false
});
}
var _style = new Style(initalStyles, onStyleChange);
/**
* Returns an object that has the properties that define the current user interface controls of
* the Publisher. You can modify the properties of this object and pass the object to the
* setStyle() method of thePublisher object. (See the documentation for
* setStyle() to see the styles that define this object.)
* @return {Object} The object that defines the styles of the Publisher.
* @see setStyle()
* @method #getStyle
* @memberOf Publisher
*/
/**
* Returns an object that has the properties that define the current user interface controls of
* the Subscriber. You can modify the properties of this object and pass the object to the
* setStyle() method of the Subscriber object. (See the documentation for
* setStyle() to see the styles that define this object.)
* @return {Object} The object that defines the styles of the Subscriber.
* @see setStyle()
* @method #getStyle
* @memberOf Subscriber
*/
// If +key+ is falsly then all styles will be returned.
self.getStyle = function(key) {
return _style.get(key);
};
/**
* Sets properties that define the appearance of some user interface controls of the Publisher.
*
* You can either pass one parameter or two parameters to this method.
* *If you pass one parameter, style, it is an object that has the following
* properties:
*
*
audioLevelDisplayMode (String) — How to display the audio level
* indicator. Possible values are: "auto" (the indicator is displayed when the
* video is disabled), "off" (the indicator is not displayed), and
* "on" (the indicator is always displayed).backgroundImageURI (String) — A URI for an image to display as
* the background image when a video is not displayed. (A video may not be displayed if
* you call publishVideo(false) on the Publisher object). You can pass an http
* or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the
* data URI scheme (instead of http or https) and pass in base-64-encrypted
* PNG data, such as that obtained from the
* Publisher.getImgData() method. For example,
* you could set the property to "data:VBORw0KGgoAA...", where the portion of
* the string after "data:" is the result of a call to
* Publisher.getImgData(). If the URL or the image data is invalid, the
* property is ignored (the attempt to set the image fails silently).buttonDisplayMode (String) — How to display the microphone
* controls. Possible values are: "auto" (controls are displayed when the
* stream is first displayed and when the user mouses over the display), "off"
* (controls are not displayed), and "on" (controls are always displayed).nameDisplayMode (String) Whether to display the stream name.
* Possible values are: "auto" (the name is displayed when the stream is first
* displayed and when the user mouses over the display), "off" (the name is not
* displayed), and "on" (the name is always displayed).For example, the following code passes one parameter to the method:
* *myPublisher.setStyle({nameDisplayMode: "off"});
*
* If you pass two parameters, style and value, they are
* key-value pair that define one property of the display style. For example, the following
* code passes two parameter values to the method:
myPublisher.setStyle("nameDisplayMode", "off");
*
* You can set the initial settings when you call the Session.publish()
* or OT.initPublisher() method. Pass a style property as part of the
* properties parameter of the method.
The OT object dispatches an exception event if you pass in an invalid style
* to the method. The code property of the ExceptionEvent object is set to 1011.
style passed in. Pass a value
* for this parameter only if the value of the style parameter is a String.
*
* @see getStyle()
* @return {Publisher} The Publisher object
* @see setStyle()
*
* @see Session.publish()
* @see OT.initPublisher()
* @method #setStyle
* @memberOf Publisher
*/
/**
* Sets properties that define the appearance of some user interface controls of the Subscriber.
*
* You can either pass one parameter or two parameters to this method.
* *If you pass one parameter, style, it is an object that has the following
* properties:
*
*
audioLevelDisplayMode (String) — How to display the audio level
* indicator. Possible values are: "auto" (the indicator is displayed when the
* video is disabled), "off" (the indicator is not displayed), and
* "on" (the indicator is always displayed).backgroundImageURI (String) — A URI for an image to display as
* the background image when a video is not displayed. (A video may not be displayed if
* you call subscribeToVideo(false) on the Publisher object). You can pass an
* http or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the
* data URI scheme (instead of http or https) and pass in base-64-encrypted
* PNG data, such as that obtained from the
* Subscriber.getImgData() method. For example,
* you could set the property to "data:VBORw0KGgoAA...", where the portion of
* the string after "data:" is the result of a call to
* Publisher.getImgData(). If the URL or the image data is invalid, the
* property is ignored (the attempt to set the image fails silently).buttonDisplayMode (String) — How to display the speaker
* controls. Possible values are: "auto" (controls are displayed when the
* stream is first displayed and when the user mouses over the display), "off"
* (controls are not displayed), and "on" (controls are always displayed).nameDisplayMode (String) Whether to display the stream name.
* Possible values are: "auto" (the name is displayed when the stream is first
* displayed and when the user mouses over the display), "off" (the name is not
* displayed), and "on" (the name is always displayed).videoDisabledDisplayMode (String) Whether to display the video
* disabled indicator and video disabled warning icons for a Subscriber. These icons
* indicate that the video has been disabled (or is in risk of being disabled for
* the warning icon) due to poor stream quality. Possible values are: "auto"
* (the icons are automatically when the displayed video is disabled or in risk of being
* disabled due to poor stream quality), "off" (do not display the icons), and
* "on" (display the icons).For example, the following code passes one parameter to the method:
* *mySubscriber.setStyle({nameDisplayMode: "off"});
*
* If you pass two parameters, style and value, they are key-value
* pair that define one property of the display style. For example, the following code passes
* two parameter values to the method:
mySubscriber.setStyle("nameDisplayMode", "off");
*
* You can set the initial settings when you call the Session.subscribe() method.
* Pass a style property as part of the properties parameter of the
* method.
The OT object dispatches an exception event if you pass in an invalid style
* to the method. The code property of the ExceptionEvent object is set to 1011.
style passed in. Pass a value
* for this parameter only if the value of the style parameter is a String.
*
* @returns {Subscriber} The Subscriber object.
*
* @see getStyle()
* @see setStyle()
*
* @see Session.subscribe()
* @method #setStyle
* @memberOf Subscriber
*/
self.setStyle = function(keyOrStyleHash, value, silent) {
if (_readOnly) {
logging.warn('Calling setStyle() has no effect because the' +
'showControls option was set to false');
return this;
}
var styleHash;
if (typeof keyOrStyleHash !== 'string') {
styleHash = keyOrStyleHash;
} else {
styleHash = {};
styleHash[keyOrStyleHash] = value;
}
if (styleHash.backgroundImageURI) {
styleHash.backgroundImageURI = fixBackgroundImageURI(styleHash.backgroundImageURI);
}
_style.setAll(styleHash, silent);
logSetStyleWithPayload(
styleHashLogFilter(styleHash)
);
return this;
};
};
},{"../logging.js":187,"./fix_background_image_uri.js":243,"./style.js":245,"./style_hash_log_filter.js":246}],245:[function(require,module,exports){
'use strict';
var cloneDeep = require('lodash.clonedeep');
var logging = require('../logging.js');
module.exports = function Style(initalStyles, onStyleChange) {
var styleApi = this;
var _styles = {};
// Styles with null here means every value is valid for that style
var _validStyleValues = {
buttonDisplayMode: ['auto', 'mini', 'mini-auto', 'off', 'on'],
nameDisplayMode: ['auto', 'off', 'on'],
audioLevelDisplayMode: ['auto', 'off', 'on'],
showSettingsButton: [true, false],
showSpeakerButton: null,
showMicButton: [true, false],
backgroundImageURI: null,
showControlBar: [true, false],
// 'showArchiveStatus' is kept for backwards compatibility.
// 'archiveStatusDisplayMode' is the documented style name.
showArchiveStatus: [true, false],
archiveStatusDisplayMode: ['auto', 'off', 'on'],
videoDisabledDisplayMode: ['auto', 'off', 'on']
};
// Validates the style +key+ and also whether +value+ is valid for +key+
var isValidStyle = function(key, value) {
var valueSpec = _validStyleValues[key];
if (valueSpec === undefined) {
return false;
}
if (valueSpec === null) {
return true;
}
return valueSpec.indexOf(value) !== -1;
};
var castValue = function(value) {
switch (value) {
case 'true':
return true;
case 'false':
return false;
default:
return value;
}
};
// Returns a copy of the styles.
styleApi.getAll = function() {
return cloneDeep(_styles);
};
styleApi.get = function(key) {
if (key) {
return _styles[key];
}
// We haven't been asked for any specific key, just return the lot
return styleApi.getAll();
};
// *note:* this will not trigger onStyleChange if +silent+ is truthy
styleApi.setAll = function(newStyles, silent) {
Object.keys(newStyles).forEach(function(key) {
styleApi.set(key, newStyles[key], silent);
});
return styleApi;
};
// *note:* this will not trigger onStyleChange if +silent+ is truthy
styleApi.set = function(key, value, silent) {
logging.debug('setStyle: ' + key.toString());
var oldValue;
var newValue = castValue(value);
if (!isValidStyle(key, newValue)) {
logging.warn('Style.set::Invalid style property passed ' + key + ' : ' + newValue);
return styleApi;
}
oldValue = _styles[key];
if (newValue !== oldValue) {
_styles[key] = newValue;
if (!silent) {
onStyleChange(key, value, oldValue);
}
}
return styleApi;
};
if (initalStyles) {
styleApi.setAll(initalStyles, true);
}
};
},{"../logging.js":187,"lodash.clonedeep":89}],246:[function(require,module,exports){
'use strict';
var cloneDeep = require('lodash.clonedeep');
// The backgroundImageURI of the styleHash object that we log to analytics needs to be stripped down
// because base64 data blobs are often very large and can be a privacy issue if taken from the
// user's camera.
module.exports = function styleHashLogFilter(styleHashInput) {
var styleHash = cloneDeep(styleHashInput);
if (styleHash.backgroundImageURI) {
var protocol = styleHash.backgroundImageURI.split(':')[0];
styleHash.backgroundImageURI = (
(['http', 'https', 'data'].indexOf(protocol) !== -1) ?
protocol :
'other'
);
}
return styleHash;
};
},{"lodash.clonedeep":89}],247:[function(require,module,exports){
(function (global){
'use strict';
var uuid = require('uuid');
var EventEmitter = require('events');
var analytics = require('../analytics.js');
var audioContext = require('../../helpers/audio_context.js');
var GetstatsAudioOutputLevelSampler = require('../../helpers/audio_level_samplers/getstats_audio_output_level_sampler');
var WebaudioAudioLevelSampler = require('../../helpers/audio_level_samplers/webaudio_audio_level_sampler');
var AudioLevelTransformer = require('../audio_level_transformer');
var AudioLevelMeter = require('../chrome/audio_level_meter.js');
var BackingBar = require('../chrome/backing_bar.js');
var Chrome = require('../chrome/chrome.js');
var ConnectivityAttemptPinger = require('../../helpers/connectivity_attempt_pinger.js');
var Events = require('../events.js');
var ExceptionCodes = require('../exception_codes.js');
var getStatsHelpers = require('../peer_connection/get_stats_helpers.js');
var IntervalRunner = require('../interval_runner.js');
var logging = require('../logging.js');
var Message = require('../messaging/raptor/message.js');
var MuteButton = require('../chrome/mute_button.js');
var NamePanel = require('../chrome/name_panel.js');
var OTError = require('../ot_error.js');
var OTHelpers = require('@opentok/ot-helpers');
var OTPlugin = require('@opentok/otplugin.js');
var properties = require('../../helpers/properties.js');
var SubscriberPeerConnection = require('../peer_connection/subscriber_peer_connection.js');
var SubscribingState = require('./state.js');
var StylableComponent = require('../styling/stylable_component.js');
var VideoDisabledIndicator = require('../chrome/video_disabled_indicator.js');
var WidgetView = require('../../helpers/widget_view.js');
var BIND_VIDEO_DELAY_MAX = 30000;
var BIND_VIDEO_DELAY_WARNING = 15000;
/**
* The Subscriber object is a representation of the local video element that is playing back
* a remote stream. The Subscriber object includes methods that let you disable and enable
* local audio playback for the subscribed stream. The subscribe() method of the
* {@link Session} object returns a Subscriber object.
*
* @property {Element} element The HTML DOM element containing the Subscriber.
* @property {String} id The DOM ID of the Subscriber.
* @property {Stream} stream The stream to which you are subscribing.
*
* @class Subscriber
* @augments EventDispatcher
*/
var Subscriber = function(targetElement, options, completionHandler) {
var _widgetView,
_streamContainer,
_chrome,
_muteDisplayMode,
_audioLevelMeter,
_fromConnectionId,
_peerConnection,
_subscribeStartTime,
_startConnectingTime,
_state,
_audioLevelSampler,
_audioLevelRunner,
_isLocalStream,
_webRTCStream,
_lastSubscribeToVideoReason,
_attemptStartTime,
_connectivityAttemptPinger,
_streamEventHandlers;
var _loaded = false;
var _widgetId = uuid();
var _domId = targetElement || _widgetId;
var _session = options.session;
var _stream = options.stream;
var _audioVolume = 100;
var _audioLevelCapable = OTHelpers.hasCapabilities('audioOutputLevelStat') ||
OTHelpers.hasCapabilities('webAudioCapableRemoteStream');
var _frameRateRestricted = false;
var _lastIceConnectionState = Events.Event.names.SUBSCRIBER_DISCONNECTED;
var _subscriber = this;
var _preDisconnectStats = {};
var _congestionLevel = null;
// used for test purposes so that we can rely on synchronously dispatched events
var _syncEventEmitter = new EventEmitter();
var _properties = OTHelpers.defaults({}, options, {
showControls: true,
testNetwork: false,
fitMode: _stream.defaultFitMode || 'cover',
insertDefaultUI: true
});
if (typeof completionHandler !== 'function') {
completionHandler = function() {};
}
this.id = _domId;
this.widgetId = _widgetId;
this.session = _session;
this.stream = _stream = _properties.stream;
this.streamId = _stream.id;
if (!_session) {
OTError.handleJsException('OT.Subscriber must be passed a session option', 2000, {
session: _session,
target: this
});
return null;
}
OTHelpers.eventing(this, false);
_subscriber.once('subscribeComplete', completionHandler);
if (_audioLevelCapable) {
this.on({
'audioLevelUpdated:added': function(count) {
if (count === 1 && _audioLevelRunner) {
_audioLevelRunner.start();
}
},
'audioLevelUpdated:removed': function(count) {
if (count === 0 && _audioLevelRunner) {
_audioLevelRunner.stop();
}
}
});
}
// make sure we trigger an error if we are not getting any "data" after a reasonable
// amount of time
var setUpBindGuards = function() {
var tos = [
setTimeout(function() {
logConnectivityEvent('Warning', { streamId: _stream.id });
}, BIND_VIDEO_DELAY_WARNING),
setTimeout(function() {
onPeerConnectionFailure(null, 'OT.Subscriber failed to subscribe to a stream in a ' +
'reasonable amount of time', _peerConnection, 'ICEWorkflow');
}, BIND_VIDEO_DELAY_MAX)
];
return function cancelBindGuards() {
tos.forEach(function(to) {
clearTimeout(to);
});
};
};
var logAnalyticsEvent = function(action, variation, payload, options, throttle) {
var stats = OTHelpers.extend({
action: action,
variation: variation,
payload: payload,
streamId: _stream ? _stream.id : null,
sessionId: _session ? _session.sessionId : null,
connectionId: (_session && _session.isConnected()) ? _session.connection.connectionId : null,
partnerId: (_session && _session.sessionInfo) ? _session.sessionInfo.partnerId : null,
subscriberId: _widgetId
}, options);
if (variation === 'Failure') {
stats = OTHelpers.extend(stats, _preDisconnectStats);
}
var args = [stats];
if (throttle) { args.push(throttle); }
analytics.logEvent.apply(analytics, args);
};
var logConnectivityEvent = function(variation, payload, options) {
if (variation === 'Attempt' || !_connectivityAttemptPinger) {
_attemptStartTime = new Date().getTime();
_connectivityAttemptPinger = new ConnectivityAttemptPinger({
action: 'Subscribe',
sessionId: _session ? _session.sessionId : null,
connectionId: _session && _session.isConnected() ?
_session.connection.connectionId : null,
partnerId: _session.isConnected() ? _session.sessionInfo.partnerId : null,
p2p: _session && _session.sessionInfo.p2pEnabled,
messagingServer: _session && _session.sessionInfo.messagingServer,
subscriberId: _widgetId,
streamId: _stream ? _stream.id : null
});
}
_connectivityAttemptPinger.setVariation(variation);
if (variation === 'Failure' || variation === 'Success' || variation === 'Cancel') {
if (!options) { options = {}; }
OTHelpers.extend(options, {
attemptDuration: new Date().getTime() - _attemptStartTime
});
}
logAnalyticsEvent('Subscribe', variation, payload, options);
};
var logResubscribe = function(variation, payload) {
logAnalyticsEvent('ICERestart', variation, payload);
};
var recordQOS = function(parsedStats) {
var domElement;
if (_widgetView && _widgetView.domElement) {
domElement = _widgetView.domElement;
} else if (_widgetView && _widgetView.video() && _widgetView.video().domElement()) {
// If we're using insertDefaultUI=false then there is no container
domElement = _widgetView.video().domElement();
}
var QoSBlob = {
widgetType: 'Subscriber',
width: (domElement ?
Number(OTHelpers.width(domElement).replace('px', '')) :
null
),
height: (
domElement ?
Number(OTHelpers.height(domElement).replace('px', '')) :
null
),
audioTrack: _webRTCStream && _webRTCStream.getAudioTracks().length > 0,
hasAudio: _stream && _stream.hasAudio,
subscribeToAudio: _properties.subscribeToAudio,
videoTrack: _webRTCStream && _webRTCStream.getVideoTracks().length > 0,
hasVideo: _stream && _stream.hasVideo,
subscribeToVideo: _properties.subscribeToVideo,
sessionId: _session ? _session.sessionId : null,
connectionId: _session ? _session.connection.connectionId : null,
mediaServerName: _session ? _session.sessionInfo.mediaServerName : null,
p2p: _session ? _session.sessionInfo.p2pEnabled : false,
messagingServer: _session ? _session.sessionInfo.messagingServer : null,
apiServer: properties.apiURL,
congestionLevel: _congestionLevel,
partnerId: _session ? _session.apiKey : null,
streamId: _stream.id,
subscriberId: _widgetId,
version: properties.version,
duration: Math.round((OTHelpers.now() - _subscribeStartTime) / 1000),
remoteConnectionId: _stream.connection.connectionId
};
analytics.logQOS(OTHelpers.extend(QoSBlob, parsedStats));
this.trigger('qos', parsedStats);
}.bind(this);
var stateChangeFailed = function(changeFailed) {
logging.error('OT.Subscriber State Change Failed: ', changeFailed.message);
logging.debug(changeFailed);
};
var onLoaded = function() {
if (_state.isSubscribing() || !_streamContainer) { return; }
_loaded = true;
logging.debug('OT.Subscriber.onLoaded');
_state.set('Subscribing');
_subscribeStartTime = OTHelpers.now();
var payload = {
pcc: parseInt(_subscribeStartTime - _startConnectingTime, 10),
hasRelayCandidates: _peerConnection && _peerConnection.hasRelayCandidates()
};
logAnalyticsEvent('createPeerConnection', 'Success', payload);
_widgetView.loading(false);
if (_chrome) {
_chrome.showAfterLoading();
}
if (_frameRateRestricted) {
_stream.setRestrictFrameRate(true);
}
if (_audioLevelMeter) {
_audioLevelMeter.audioOnly(_widgetView.audioOnly());
}
this.trigger('subscribeComplete', null, this);
this.trigger('loaded', this);
logConnectivityEvent('Success', { streamId: _stream.id });
};
var onDisconnected = function() {
logging.debug('OT.Subscriber has been disconnected from the Publisher\'s PeerConnection');
if (_state.isAttemptingToSubscribe()) {
// subscribing error
_state.set('Failed');
this.trigger('subscribeComplete', new OTError(null, 'ClientDisconnected'), this);
} else if (_state.isSubscribing()) {
_state.set('Failed');
// we were disconnected after we were already subscribing
// probably do nothing?
}
this.disconnect();
};
var onPeerConnectionFailure = function(code, reason, peerConnection, prefix) {
if (prefix === 'ICEWorkflow' && _session.sessionInfo.reconnection && _loaded) {
logging.debug('Ignoring peer connection failure due to possibility of reconnection.');
return;
}
var errorCode;
if (prefix === 'ICEWorkflow') {
errorCode = ExceptionCodes.SUBSCRIBER_ICE_WORKFLOW_FAILED;
} else {
errorCode = ExceptionCodes.P2P_CONNECTION_FAILED;
}
var options = {
failureReason: prefix ? prefix : 'PeerConnectionError',
failureMessage: 'OT.Subscriber PeerConnection Error: ' + reason,
failureCode: errorCode
};
if (_state.isAttemptingToSubscribe()) {
// We weren't subscribing yet so this was a failure in setting
// up the PeerConnection or receiving the initial stream.
var payload = {
hasRelayCandidates: _peerConnection && _peerConnection.hasRelayCandidates()
};
logAnalyticsEvent('createPeerConnection', 'Failure', payload, options);
_state.set('Failed');
this.trigger('subscribeComplete', new OTError(null, reason), this);
logConnectivityEvent('Failure', null, options);
} else if (_state.isSubscribing()) {
// we were disconnected after we were already subscribing
_state.set('Failed');
this.trigger('error', reason);
logConnectivityEvent('Error', null, options);
}
this.disconnect();
OTError.handleJsException('OT.Subscriber PeerConnection Error: ' + reason,
errorCode, {
session: _session,
target: this
}
);
_showError.call(this, reason);
}.bind(this);
var onRemoteStreamAdded = function(webRTCStream) {
_webRTCStream = webRTCStream;
logging.debug('OT.Subscriber.onRemoteStreamAdded');
_state.set('BindingRemoteStream');
// Disable the audio/video, if needed
this.subscribeToAudio(_properties.subscribeToAudio);
_lastSubscribeToVideoReason = 'loading';
this.subscribeToVideo(_properties.subscribeToVideo, 'loading');
// setting resolution and frame rate doesn't work in P2P
if (!_session.sessionInfo.p2pEnabled) {
this.setPreferredResolution(_properties.preferredResolution);
this.setPreferredFrameRate(_properties.preferredFrameRate);
}
var videoContainerOptions = {
error: onPeerConnectionFailure,
audioVolume: _audioVolume
};
// This is a workaround for a bug in Chrome where a track disabled on
// the remote end doesn't fire loadedmetadata causing the subscriber to timeout
// https://jira.tokbox.com/browse/OPENTOK-15605
// Also https://jira.tokbox.com/browse/OPENTOK-16425
// Also https://tokbox.atlassian.net/browse/OPENTOK-27112
//
// Workaround for an IE issue https://jira.tokbox.com/browse/OPENTOK-18824
// We still need to investigate further.
//
webRTCStream.getVideoTracks().forEach(function(track) {
if (global.webkitMediaStream) {
track.enabled = false;
} else {
track.enabled = _stream.hasVideo && _properties.subscribeToVideo;
}
});
_streamContainer = _widgetView.bindVideo(webRTCStream, videoContainerOptions,
function(err) {
if (err) {
onPeerConnectionFailure(null, err.message || err, _peerConnection, 'VideoElement');
return;
}
if (global.webkitMediaStream) {
// Reenable any video streams that we previously disabled for OPENTOK-27112
webRTCStream.getVideoTracks().forEach(function(track) {
track.enabled = _stream.hasVideo && _properties.subscribeToVideo;
});
}
if (_streamContainer) {
_streamContainer.orientation({
width: _stream.videoDimensions.width,
height: _stream.videoDimensions.height,
videoOrientation: _stream.videoDimensions.orientation
});
}
onLoaded.call(this, null);
}.bind(this));
// if the audioLevelSampler implementation requires a stream we need to set it now
if (_audioLevelSampler && 'webRTCStream' in _audioLevelSampler
&& webRTCStream.getAudioTracks().length > 0) {
_audioLevelSampler.webRTCStream(webRTCStream);
}
logAnalyticsEvent('createPeerConnection', 'StreamAdded');
this.trigger('streamAdded', this);
// for test purposes
_syncEventEmitter.emit('__sync__streamAdded', this);
};
var onRemoteStreamRemoved = function(webRTCStream) {
_webRTCStream = null;
logging.debug('OT.Subscriber.onStreamRemoved');
if (_streamContainer.stream === webRTCStream) {
_streamContainer.destroy();
_streamContainer = null;
}
this.trigger('streamRemoved', this);
};
var connectionStateMap = {
new: Events.Event.names.SUBSCRIBER_DISCONNECTED,
checking: Events.Event.names.SUBSCRIBER_DISCONNECTED,
connected: Events.Event.names.SUBSCRIBER_CONNECTED,
completed: Events.Event.names.SUBSCRIBER_CONNECTED,
disconnected: Events.Event.names.SUBSCRIBER_DISCONNECTED
};
var onIceConnectionStateChange = function(state) {
var mappedState = connectionStateMap[state];
if (mappedState && mappedState !== _lastIceConnectionState) {
_lastIceConnectionState = mappedState;
logging.debug('OT.Subscriber.connectionStateChanged to ' + state);
this.dispatchEvent(new Events.ConnectionStateChangedEvent(mappedState, this));
}
};
var onIceRestartSuccess = function() {
logResubscribe('Success');
};
var onIceRestartFailure = function() {
logResubscribe('Failure', {
reason: 'ICEWorkflow',
message: 'OT.Subscriber PeerConnection Error: ' +
'The stream was unable to connect due to a network error.' +
' Make sure your connection isn\'t blocked by a firewall.'
});
};
var streamDestroyed = function() {
this.disconnect();
};
var streamUpdated = function(event) {
switch (event.changedProperty) {
case 'videoDimensions':
if (!_streamContainer) {
// Ignore videoEmension updates before streamContainer is created OPENTOK-17253
break;
}
_streamContainer.orientation({
width: event.newValue.width,
height: event.newValue.height,
videoOrientation: event.newValue.orientation
});
this.dispatchEvent(new Events.VideoDimensionsChangedEvent(
this, event.oldValue, event.newValue
));
break;
case 'videoDisableWarning':
if (_chrome) {
_chrome.videoDisabledIndicator.setWarning(event.newValue);
}
this.dispatchEvent(new Events.VideoDisableWarningEvent(
event.newValue ? 'videoDisableWarning' : 'videoDisableWarningLifted'
));
_congestionLevel = event.newValue === 'videoDisableWarning' ? 1 : null;
break;
case 'hasVideo':
setAudioOnly(!(_stream.hasVideo && _properties.subscribeToVideo));
this.dispatchEvent(new Events.VideoEnabledChangedEvent(
_stream.hasVideo ? 'videoEnabled' : 'videoDisabled',
{
reason: 'publishVideo'
}
));
break;
case 'hasAudio':
_muteDisplayMode.update();
break;
default:
}
};
/// Chrome
_muteDisplayMode = {
get: function() {
return _stream.hasAudio ? _subscriber.getStyle('buttonDisplayMode') : 'off';
},
update: function() {
var mode = _muteDisplayMode.get();
if (_chrome) {
_chrome.muteButton.setDisplayMode(mode);
_chrome.backingBar.setMuteMode(mode);
}
}
};
var updateChromeForStyleChange = function(key, value/*, oldValue*/) {
if (!_chrome) { return; }
switch (key) {
case 'nameDisplayMode':
_chrome.name.setDisplayMode(value);
_chrome.backingBar.setNameMode(value);
break;
case 'videoDisabledDisplayMode':
_chrome.videoDisabledIndicator.setDisplayMode(value);
break;
case 'showArchiveStatus':
_chrome.archive.setShowArchiveStatus(value);
break;
case 'buttonDisplayMode':
_muteDisplayMode.update();
break;
case 'audioLevelDisplayMode':
_chrome.audioLevel.setDisplayMode(value);
break;
case 'bugDisplayMode':
// bugDisplayMode can't be updated but is used by some partners
break;
case 'backgroundImageURI':
_widgetView.setBackgroundImageURI(value);
break;
default:
}
};
var _createChrome = function() {
var widgets = {
backingBar: new BackingBar({
nameMode: !_properties.name ? 'off' : this.getStyle('nameDisplayMode'),
muteMode: _muteDisplayMode.get()
}),
name: new NamePanel({
name: _properties.name,
mode: this.getStyle('nameDisplayMode')
}),
muteButton: new MuteButton({
muted: _properties.muted,
mode: _muteDisplayMode.get()
})
};
if (_audioLevelCapable) {
var audioLevelTransformer = new AudioLevelTransformer();
var audioLevelUpdatedHandler = function(evt) {
_audioLevelMeter.setValue(audioLevelTransformer.transform(evt.audioLevel));
};
_audioLevelMeter = new AudioLevelMeter({
mode: _subscriber.getStyle('audioLevelDisplayMode')
});
_audioLevelMeter.watchVisibilityChanged(function(visible) {
if (visible) {
_subscriber.on('audioLevelUpdated', audioLevelUpdatedHandler);
} else {
_subscriber.off('audioLevelUpdated', audioLevelUpdatedHandler);
}
});
_audioLevelMeter.setDisplayMode(this.getStyle('audioLevelDisplayMode'));
_audioLevelMeter.audioOnly(false);
widgets.audioLevel = _audioLevelMeter;
}
widgets.videoDisabledIndicator = new VideoDisabledIndicator({
mode: this.getStyle('videoDisabledDisplayMode')
});
if (_widgetView && _widgetView.domElement) {
_chrome = new Chrome({
parent: _widgetView.domElement
}).set(widgets).on({
muted: function() {
muteAudio.call(this, true);
},
unmuted: function() {
muteAudio.call(this, false);
}
}, this);
// Hide the chrome until we explicitly show it
_chrome.hideWhileLoading();
}
};
var _showError = function() {
// Display the error message inside the container, assuming it's
// been created by now.
if (_widgetView) {
_widgetView.addError(
'The stream was unable to connect due to a network error.',
'Make sure your connection isn\'t blocked by a firewall.'
);
}
};
StylableComponent(this, {
nameDisplayMode: 'auto',
buttonDisplayMode: 'auto',
audioLevelDisplayMode: 'auto',
videoDisabledDisplayMode: 'auto',
backgroundImageURI: null,
showArchiveStatus: true,
showMicButton: true
}, _properties.showControls, function(payload) {
logAnalyticsEvent('SetStyle', 'Subscriber', payload, null, 0.1);
});
var setAudioOnly = function(audioOnly) {
if (_peerConnection) {
_peerConnection.subscribeToVideo(!audioOnly);
}
if (_widgetView) {
_widgetView.audioOnly(audioOnly);
_widgetView.showPoster(audioOnly);
}
if (_audioLevelMeter) {
_audioLevelMeter.audioOnly(audioOnly);
}
};
// logs an analytics event for getStats on the first call
var notifyGetStatsCalled = (function() {
var callCount = 0;
return function throttlingNotifyGetStatsCalled() {
if (callCount === 0) {
logAnalyticsEvent('GetStats', 'Called');
}
callCount++;
};
})();
this.destroy = function(reason, quiet) {
if (_state.isDestroyed()) { return this; }
if (_state.isAttemptingToSubscribe()) {
var payload = {
streamId: _stream.id,
reason: reason || 'destroy'
};
if (reason === 'streamDestroyed') {
// We weren't subscribing yet so the stream was destroyed before we setup
// the PeerConnection or receiving the initial stream.
this.trigger('subscribeComplete', new OTError(null, 'InvalidStreamID'), this);
} else {
_connectivityAttemptPinger.stop();
}
logConnectivityEvent('Cancel', payload);
}
_state.set('Destroyed');
if (_audioLevelRunner) {
_audioLevelRunner.stop();
}
_preDisconnectStats = {
sessionId: _session.sessionId,
connectionId: (_session && _session.isConnected()) ? _session.connection.connectionId : null,
partnerId: (_session && _session.sessionInfo) ? _session.sessionInfo.partnerId : null
};
this.disconnect();
if (_chrome) {
_chrome.destroy();
_chrome = null;
}
if (_widgetView) {
_widgetView.destroy();
_widgetView = null;
this.element = null;
}
if (_stream && !_stream.destroyed) {
logAnalyticsEvent('unsubscribe', null, { streamId: _stream.id });
}
_stream.off(_streamEventHandlers, this);
this.id = _domId = null;
this.stream = _stream = null;
this.streamId = null;
this.session = _session = null;
_properties = null;
if (quiet !== true) {
this.dispatchEvent(
new Events.DestroyedEvent(
Events.Event.names.SUBSCRIBER_DESTROYED,
this,
reason
),
this.off.bind(this)
);
}
return this;
};
this.disconnect = function() {
if (!_state.isDestroyed() && !_state.isFailed()) {
// If we are already in the destroyed state then disconnect
// has been called after (or from within) destroy.
_state.set('NotSubscribing');
}
if (_streamContainer) {
_streamContainer.destroy();
_streamContainer = null;
}
if (_peerConnection) {
_peerConnection.destroy();
_peerConnection = null;
logAnalyticsEvent('disconnect', 'PeerConnection', { streamId: _stream.id });
}
};
this.processMessage = function(type, fromConnection, message) {
logging.debug('OT.Subscriber.processMessage: Received ' + type + ' message from ' +
fromConnection.id);
logging.debug(message);
if (_fromConnectionId !== fromConnection.id) {
_fromConnectionId = fromConnection.id;
}
if (_peerConnection) {
_peerConnection.processMessage(type, message);
}
};
this.disableVideo = function(active) {
if (!active) {
logging.warn('Due to high packet loss and low bandwidth, video has been disabled');
} else if (_lastSubscribeToVideoReason === 'auto') {
logging.info('Video has been re-enabled');
if (_chrome) {
_chrome.videoDisabledIndicator.disableVideo(false);
}
} else {
logging.info('Video was not re-enabled because it was manually disabled');
return;
}
this.subscribeToVideo(active, 'auto');
if (!active && _chrome) {
_chrome.videoDisabledIndicator.disableVideo(true);
}
var payload = active ? { videoEnabled: true } : { videoDisabled: true };
logAnalyticsEvent('updateQuality', 'video', payload);
};
/**
* Returns the base-64-encoded string of PNG data representing the Subscriber video.
*
* You can use the string as the value for a data URL scheme passed to the src parameter of * an image file, as in the following:
* *
* var imgData = subscriber.getImgData();
*
* var img = document.createElement("img");
* img.setAttribute("src", "data:image/png;base64," + imgData);
* var imgWin = window.open("about:blank", "Screenshot");
* imgWin.document.write("<body></body>");
* imgWin.document.body.appendChild(img);
*
* @method #getImgData
* @memberOf Subscriber
* @return {String} The base-64 encoded string. Returns an empty string if there is no video.
*/
this.getImgData = function() {
if (!this.isSubscribing()) {
logging.error('OT.Subscriber.getImgData: Cannot getImgData before the Subscriber ' +
'is subscribing.');
return null;
}
return _streamContainer.imgData();
};
/**
* Returns the details on the subscriber stream quality, including the following:
*
* testNetwork property to true in the options you pass into the
* Session.subscribe() method.
* * You may also use these statistics to have a Subscriber subscribe to audio-only if the audio * packet loss reaches a certain threshold. If you choose to do this, you should set the * `audioFallbackEnabled` setting to `false` when you initialize Publisher objects for the session. * This prevents the OpenTok Media Router from using its own audio-only toggling implementation. * (See the documentation for the OT.initPublisher() method.) * * @param {Function} completionHandler A function that takes the following * parameters: * *
error (Error) — Upon successful completion of
* the network test, this is undefined. The error results if the client is not connected or
* the stream published by your own client.stats (Object) — An object with the following properties:
* *
audio.bytesReceived (Number) — The total nubmer of audio bytes
* received by the subscriberaudio.packetsLost (Number) — Total audio packets that did not reach
* the subscriberaudio.packetsReceived (Number) — The total number of audio packets
* received by the subscribertimestamp (Number) — The timestamp, in milliseconds since the Unix
* epoch, for when these stats were gatheredvideo.bytesReceived (Number) — The total video bytes received by
* the subscribervideo.packetsLost (Number) — The total number of video packets that
* did not reach the subscribervideo.packetsReceived (Number) — The total number of video packets
* received by the subscriberYou can set the initial volume when you call the Session.subscribe()
* method. Pass a audioVolume property of the properties parameter
* of the method.
mySubscriber.setAudioVolume(50).setStyle(newStyle);* * @see getAudioVolume() * @see Session.subscribe() * @method #setAudioVolume * @memberOf Subscriber */ this.setAudioVolume = function(value) { value = parseInt(value, 10); if (isNaN(value)) { logging.error('OT.Subscriber.setAudioVolume: value should be an integer between 0 and 100'); return this; } _audioVolume = Math.max(0, Math.min(100, value)); if (_audioVolume !== value) { logging.warn('OT.Subscriber.setAudioVolume: value should be an integer between 0 and 100'); } if (_properties.muted && _audioVolume > 0) { _properties.premuteVolume = value; muteAudio.call(this, false); } if (_streamContainer) { _streamContainer.setAudioVolume(_audioVolume); } return this; }; /** * Returns the audio volume, between 0 and 100, of the Subscriber. * *
Generally you use this method in conjunction with the setAudioVolume()
* method.
value is true; stops
* subscribing to audio (if it is currently being subscribed to) when the value
* is false.
* * Note: This method only affects the local playback of audio. It has no impact on the * audio for other connections subscribing to the same stream. If the Publsher is not * publishing audio, enabling the Subscriber audio will have no practical effect. *
* * @param {Boolean} value Whether to start subscribing to audio (true) or not
* (false).
*
* @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the
* following:
*
* mySubscriber.subscribeToAudio(true).subscribeToVideo(false);* * @see subscribeToVideo() * @see Session.subscribe() * @see StreamPropertyChangedEvent * * @method #subscribeToAudio * @memberOf Subscriber */ this.subscribeToAudio = function(pValue) { var value = OTHelpers.castToBoolean(pValue, true); if (_peerConnection) { _peerConnection.subscribeToAudio(value && !_properties.subscribeMute); if (_session && _stream && value !== _properties.subscribeToAudio) { _stream.setChannelActiveState('audio', value && !_properties.subscribeMute); } } _properties.subscribeToAudio = value; return this; }; var muteAudio = function(_mute) { if (_chrome) { _chrome.muteButton.muted(_mute); } if (_mute === _properties.mute) { return; } if (OTHelpers.env.name === 'Chrome' || OTPlugin.isInstalled()) { _properties.subscribeMute = _properties.muted = _mute; this.subscribeToAudio(_properties.subscribeToAudio); } else if (_mute) { _properties.premuteVolume = this.getAudioVolume(); _properties.muted = true; this.setAudioVolume(0); } else if (_properties.premuteVolume || _properties.audioVolume) { _properties.muted = false; this.setAudioVolume(_properties.premuteVolume || _properties.audioVolume); } _properties.mute = _properties.mute; }; var reasonMap = { auto: 'quality', publishVideo: 'publishVideo', subscribeToVideo: 'subscribeToVideo' }; /** * Toggles video on and off. Starts subscribing to video (if it is available and * currently not being subscribed to) when the
value is true;
* stops subscribing to video (if it is currently being subscribed to) when the
* value is false.
* * Note: This method only affects the local playback of video. It has no impact on * the video for other connections subscribing to the same stream. If the Publsher is not * publishing video, enabling the Subscriber video will have no practical video. *
* * @param {Boolean} value Whether to start subscribing to video (true) or not
* (false).
*
* @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the
* following:
*
* mySubscriber.subscribeToVideo(true).subscribeToAudio(false);* * @see subscribeToAudio() * @see Session.subscribe() * @see StreamPropertyChangedEvent * * @method #subscribeToVideo * @memberOf Subscriber */ this.subscribeToVideo = function(pValue, reason) { var value = OTHelpers.castToBoolean(pValue, true); setAudioOnly(!(value && _stream.hasVideo)); if (value && _widgetView && _widgetView.video() && _stream.hasVideo) { _widgetView.loading(value); _widgetView.video().whenTimeIncrements(function() { _widgetView.loading(false); }, this); } if (_chrome && _chrome.videoDisabledIndicator) { _chrome.videoDisabledIndicator.disableVideo(false); } if (_peerConnection) { if (_session && _stream && (value !== _properties.subscribeToVideo || reason !== _lastSubscribeToVideoReason)) { _stream.setChannelActiveState('video', value, reason); } } _properties.subscribeToVideo = value; _lastSubscribeToVideoReason = reason; if (reason !== 'loading') { this.dispatchEvent(new Events.VideoEnabledChangedEvent( value ? 'videoEnabled' : 'videoDisabled', { reason: reasonMap[reason] || 'subscribeToVideo' } )); if (value === 'videoDisabled' && reason === 'auto') { _congestionLevel = 2; } } return this; }; /** * Sets the preferred resolution of the subscriber's video. This method only applies to a video * that is published using the scalable video beta feature. Lowering the preferred resolution * lowers video quality on the subscribing client, but it also reduces network and CPU usage. * You may want to use a lower resolution based on the dimensions of subscriber's video on * the web page. You may want to use a resolution rate for a subscriber to a stream that is less * important (and smaller) than other streams. *
* Not every frame rate is available to a subscriber. When you set the preferred resolution for
* the subscriber, OpenTok.js picks the best resolution available that matches your setting.
* The resolutions available are based on the value of the Subscriber object's
* stream.resolution property, which represents the maximum resolution available for
* the stream. The actual resolutions available depend, dynamically, on network and CPU resources
* available to the publisher and subscriber.
*
* You can set the initial preferred resolution used by setting the
* preferredResolution property of the options object you pass into the
* Session.subscribe() method.
*
* To participate in the scalable video beta program, see the
* OpenTok Beta programs page.
*
* @param {Object} resolution Set this to an object with two properties: width and
* height (both numbers), such as {width: 320, height: 240}. Set this to
* null to remove the preferred resolution, and the client will use the highest
* resolution available.
*
* @see Subscriber.setPreferredFrameRate()
* @see Session.subscribe()
*
* @method #setPreferredResolution
* @memberOf Subscriber
*/
this.setPreferredResolution = function(preferredResolution) {
if (_state.isDestroyed() || !_peerConnection) {
logging.warn('Cannot set the max Resolution when not subscribing to a publisher');
return;
}
_properties.preferredResolution = preferredResolution;
if (_session.sessionInfo.p2pEnabled) {
logging.warn('OT.Subscriber.setPreferredResolution will not work in a P2P Session');
return;
}
var curMaxResolution = _stream.getPreferredResolution();
var isUnchanged = (curMaxResolution && preferredResolution &&
curMaxResolution.width === preferredResolution.width &&
curMaxResolution.height === preferredResolution.height) ||
(!curMaxResolution && !preferredResolution);
if (isUnchanged) {
return;
}
_stream.setPreferredResolution(preferredResolution);
};
/**
* Sets the preferred frame rate of the subscriber's video. This method only applies to a video
* that was published using the scalable video beta feature. Lowering the preferred frame rate
* lowers video quality on the subscribing client, but it also reduces network and CPU usage.
* You may want to use a lower frame rate for a subscriber to a stream that is less important
* than other streams.
*
* Not every frame rate is available to a subscriber. When you set the preferred frame rate for
* the subscriber, OpenTok.js picks the best frame rate available that matches your setting.
* The frame rates available are based on the value of the Subscriber object's
* stream.frameRate property, which represents the maximum value available for the
* stream. The actual frame rates available depend, dynamically, on network and CPU resources
* available to the publisher and subscriber.
*
* You can set the initial preferred frame rate used by setting the preferredFrameRate
* property of the options object you pass into the Session.subscribe()
* method.
*
* To participate in the scalable video beta program, see the
* OpenTok Beta programs page.
*
* @param {Number} frameRate Set this to the desired frame rate (in frames per second). Set this to
* null to remove the preferred frame rate, and the client will use the highest
* frame rate available.
*
* @see Subscriber.setPreferredResolution()
* @see Session.subscribe()
*
* @method #setPreferredFrameRate
* @memberOf Subscriber
*/
this.setPreferredFrameRate = function(preferredFrameRate) {
if (_state.isDestroyed() || !_peerConnection) {
logging.warn('Cannot set the max frameRate when not subscribing to a publisher');
return;
}
_properties.preferredFrameRate = preferredFrameRate;
if (_session.sessionInfo.p2pEnabled) {
logging.warn('OT.Subscriber.setPreferredFrameRate will not work in a P2P Session');
return;
}
/* jshint -W116 */
if (_stream.getPreferredFrameRate() === preferredFrameRate) {
return;
}
/* jshint +W116 */
_stream.setPreferredFrameRate(preferredFrameRate);
};
this.isSubscribing = function() {
return _state.isSubscribing();
};
this.isWebRTC = true;
this.isLoading = function() {
return _widgetView && _widgetView.loading();
};
this.videoElement = function() {
return _streamContainer.domElement();
};
/**
* Returns the width, in pixels, of the Subscriber video.
*
* @method #videoWidth
* @memberOf Subscriber
* @return {Number} the width, in pixels, of the Subscriber video.
*/
this.videoWidth = function() {
return _streamContainer.videoWidth();
};
/**
* Returns the height, in pixels, of the Subscriber video.
*
* @method #videoHeight
* @memberOf Subscriber
* @return {Number} the width, in pixels, of the Subscriber video.
*/
this.videoHeight = function() {
return _streamContainer.videoHeight();
};
/**
* Restricts the frame rate of the Subscriber's video stream, when you pass in
* true. When you pass in false, the frame rate of the video stream
* is not restricted.
*
* When the frame rate is restricted, the Subscriber video frame will update once or less per * second. *
* This feature is only available in sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed), not in sessions with the media mode set to relayed. In relayed sessions, * calling this method has no effect. *
* Restricting the subscriber frame rate has the following benefits: *
* Reducing a subscriber's frame rate has no effect on the frame rate of the video in
* other clients.
*
* @param {Boolean} value Whether to restrict the Subscriber's video frame rate
* (true) or not (false).
*
* @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the
* following:
*
*
mySubscriber.restrictFrameRate(false).subscribeToAudio(true);* * @method #restrictFrameRate * @memberOf Subscriber */ this.restrictFrameRate = function(val) { logging.debug('OT.Subscriber.restrictFrameRate(' + val + ')'); logAnalyticsEvent('restrictFrameRate', val.toString(), { streamId: _stream.id }); if (_session.sessionInfo.p2pEnabled) { logging.warn('OT.Subscriber.restrictFrameRate: Cannot restrictFrameRate on a P2P session'); } if (typeof val !== 'boolean') { logging.error( 'OT.Subscriber.restrictFrameRate: expected a boolean value got a ' + typeof val ); } else { _frameRateRestricted = val; _stream.setRestrictFrameRate(val); } return this; }; this.on('styleValueChanged', updateChromeForStyleChange, this); this._ = { getDataChannel: function(label, options, completion) { // @fixme this will fail if it's called before we have a SubscriberPeerConnection. // I.e. before we have a publisher connection. if (!_peerConnection) { completion( new OTHelpers.Error('Cannot create a DataChannel before there is a publisher connection.') ); return; } _peerConnection.getDataChannel(label, options, completion); }, iceRestart: function() { if (!_peerConnection) { logging.debug('Subscriber: Skipping ice restart, we have no peer connection'); } else if (!_peerConnection.iceConnectionStateIsConnected()) { logResubscribe('Attempt'); _peerConnection.createOfferWithIceRestart(); } else { logging.debug('Subscriber: Skipping ice restart, we are connected.'); } }, syncEventEmitter: _syncEventEmitter }; _state = new SubscribingState(stateChangeFailed); logging.debug('OT.Subscriber: subscribe to ' + _stream.id); _state.set('Init'); if (!_stream) { // @todo error logging.error('OT.Subscriber: No stream parameter.'); return false; } _streamEventHandlers = { updated: streamUpdated, destroyed: streamDestroyed }; _stream.on(_streamEventHandlers, this); _fromConnectionId = _stream.connection.id; _properties.name = _properties.name || _stream.name; _properties.classNames = 'OT_root OT_subscriber'; if (_properties.style) { this.setStyle(_properties.style, null, true); } if (_properties.audioVolume) { this.setAudioVolume(_properties.audioVolume); } _properties.subscribeToAudio = OTHelpers.castToBoolean(_properties.subscribeToAudio, true); _properties.subscribeToVideo = OTHelpers.castToBoolean(_properties.subscribeToVideo, true); _widgetView = new Subscriber.WidgetView(targetElement, _properties); this.id = _domId = _widgetView.domId(); this.element = _widgetView.domElement; _widgetView.on('videoElementCreated', function(element) { var event = new Events.VideoElementCreatedEvent(element); this.dispatchEvent(event); }.bind(this)); if (this.element) { // Only create the chrome if there is an element to insert it in // for insertDefautlUI:false we don't create the chrome _createChrome.call(this); } _startConnectingTime = OTHelpers.now(); logAnalyticsEvent('createPeerConnection', 'Attempt'); _isLocalStream = _stream.connection.id === _session.connection.id; if (!_properties.testNetwork && _isLocalStream) { // Subscribe to yourself edge-case var publisher = _session.getPublisherForStream(_stream); if (!(publisher && publisher._.webRtcStream())) { this.trigger('subscribeComplete', new OTError(null, 'InvalidStreamID'), this); return this; } onRemoteStreamAdded.call(this, publisher._.webRtcStream()); } else { if (_properties.testNetwork) { this.setAudioVolume(0); } _state.set('ConnectingToPeer'); var uri = Message.subscribers.uri(_session.apiKey, _session.sessionId, _stream.id, this.widgetId); _peerConnection = new Subscriber.SubscriberPeerConnection(_stream.connection, _session, _stream, this, uri, _properties, logAnalyticsEvent); _peerConnection.on({ disconnected: onDisconnected, error: onPeerConnectionFailure, remoteStreamAdded: onRemoteStreamAdded, remoteStreamRemoved: onRemoteStreamRemoved, qos: recordQOS, iceConnectionStateChange: onIceConnectionStateChange, iceRestartSuccess: onIceRestartSuccess, iceRestartFailure: onIceRestartFailure }, this); // initialize the peer connection AFTER we've added the event listeners _peerConnection.init(function(err) { if (err) { throw err; } }); if (OTHelpers.hasCapabilities('audioOutputLevelStat')) { _audioLevelSampler = new GetstatsAudioOutputLevelSampler(_peerConnection.getStats); } else if (OTHelpers.hasCapabilities('webAudioCapableRemoteStream')) { _audioLevelSampler = new WebaudioAudioLevelSampler(audioContext()); } if (_audioLevelSampler) { var subscriber = this; // sample with interval to minimise disturbance on animation loop but dispatch the // event with RAF since the main purpose is animation of a meter _audioLevelRunner = new IntervalRunner(function() { _audioLevelSampler.sample(function(audioOutputLevel) { if (audioOutputLevel !== null) { OTHelpers.requestAnimationFrame(function() { subscriber.dispatchEvent( new Events.AudioLevelUpdatedEvent(audioOutputLevel)); }); } }); }, 60); } } var cancelBindGuards = setUpBindGuards(); _subscriber.once('subscribeComplete destroyed', cancelBindGuards); logConnectivityEvent('Attempt', { streamId: _stream.id }); /** * Dispatched periodically to indicate the subscriber's audio level. The event is dispatched * up to 60 times per second, depending on the browser. The
audioLevel property
* of the event is audio level, from 0 to 1.0. See {@link AudioLevelUpdatedEvent} for more
* information.
* * The following example adjusts the value of a meter element that shows volume of the * subscriber. Note that the audio level is adjusted logarithmically and a moving average * is applied: *
* var movingAvg = null;
* subscriber.on('audioLevelUpdated', function(event) {
* if (movingAvg === null || movingAvg <= event.audioLevel) {
* movingAvg = event.audioLevel;
* } else {
* movingAvg = 0.7 * movingAvg + 0.3 * event.audioLevel;
* }
*
* // 1.5 scaling to map the -30 - 0 dBm range to [0,1]
* var logLevel = (Math.log(movingAvg) / Math.LN10) / 1.5 + 1;
* logLevel = Math.min(Math.max(logLevel, 0), 1);
* document.getElementById('subscriberMeter').value = logLevel;
* });
*
* This example shows the algorithm used by the default audio level indicator displayed * in an audio-only Subscriber. * * @name audioLevelUpdated * @event * @memberof Subscriber * @see AudioLevelUpdatedEvent */ /** * Dispatched when the video for the subscriber is disabled. *
* The reason property defines the reason the video was disabled. This can be set to
* one of the following values:
*
* *
"publishVideo" — The publisher stopped publishing video by calling
* publishVideo(false)."quality" — The OpenTok Media Router stopped sending video
* to the subscriber based on stream quality changes. This feature of the OpenTok Media
* Router has a subscriber drop the video stream when connectivity degrades. (The subscriber
* continues to receive the audio stream, if there is one.)
*
* Before sending this event, when the Subscriber's stream quality deteriorates to a level
* that is low enough that the video stream is at risk of being disabled, the Subscriber
* dispatches a videoDisableWarning event.
*
* If connectivity improves to support video again, the Subscriber object dispatches
* a videoEnabled event, and the Subscriber resumes receiving video.
*
* By default, the Subscriber displays a video disabled indicator when a
* videoDisabled event with this reason is dispatched and removes the indicator
* when the videoDisabled event with this reason is dispatched. You can control
* the display of this icon by calling the setStyle() method of the Subscriber,
* setting the videoDisabledDisplayMode property(or you can set the style when
* calling the Session.subscribe() method, setting the style property
* of the properties parameter).
*
* This feature is only available in sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed), not in sessions with the media mode set to relayed. *
* You can disable this audio-only fallback feature, by setting the
* audioFallbackEnabled property to false in the options you pass
* into the OT.initPublisher() method on the publishing client. (See
* OT.initPublisher().)
*
"subscribeToVideo" — The subscriber started or stopped subscribing to
* video, by calling subscribeToVideo(false).
* videoDisabled event.
*
* By default, the Subscriber displays a video disabled warning indicator when this event
* is dispatched (and the video is disabled). You can control the display of this icon by
* calling the setStyle() method and setting the
* videoDisabledDisplayMode property (or you can set the style when calling
* the Session.subscribe() method and setting the style property
* of the properties parameter).
*
* This feature is only available in sessions that use the OpenTok Media Router (sessions with
* the media mode
* set to routed), not in sessions with the media mode set to relayed.
*
* @see Event
* @see videoDisabled event
* @see videoDisableWarningLifted event
* @name videoDisableWarning
* @event
* @memberof Subscriber
*/
/**
* Dispatched when the Subscriber's video element is created. Add a listener for this event when
* you set the insertDefaultUI option to false in the call to the
* Session.subscribe() method. The element
* property of the event object is a reference to the Subscriber's video element
* (or in Internet Explorer the object element containing the video). Add it to
* the HTML DOM to display the video. When you set the insertDefaultUI option to
* false, the video (or object) element is not automatically
* inserted into the DOM.
*
* Add a listener for this event only if you have set the insertDefaultUI option to
* false. If you have not set insertDefaultUI option to
* false, do not move the video (or object) element in
* in the HTML DOM. Doing so causes the Subscriber object to be destroyed.
*
* @name videoElementCreated
* @event
* @memberof Subscriber
* @see VideoElementCreatedEvent
*/
/**
* Dispatched when the OpenTok Media Router determines that the stream quality has improved
* to the point at which the video being disabled is not an immediate risk. This event is
* dispatched after the Subscriber object dispatches a videoDisableWarning event.
*
* This feature is only available in sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed), not in sessions with the media mode set to relayed. * * @see Event * @see videoDisableWarning event * @see videoDisabled event * @name videoDisableWarningLifted * @event * @memberof Subscriber */ /** * Dispatched when the OpenTok Media Router resumes sending video to the subscriber * after video was previously disabled. *
* The reason property defines the reason the video was enabled. This can be set to
* one of the following values:
*
* *
"publishVideo" — The publisher started publishing video by calling
* publishVideo(true)."quality" — The OpenTok Media Router resumed sending video
* to the subscriber based on stream quality changes. This feature of the OpenTok Media
* Router has a subscriber drop the video stream when connectivity degrades and then resume
* the video stream if the stream quality improves.
* * This feature is only available in sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed), not in sessions with the media mode set to relayed. *
"subscribeToVideo" — The subscriber started or stopped subscribing to
* video, by calling subscribeToVideo(false).
*
* To prevent video from resuming, in the videoEnabled event listener,
* call subscribeToVideo(false) on the Subscriber object.
*
* @see VideoEnabledChangedEvent
* @see videoDisabled event
* @name videoEnabled
* @event
* @memberof Subscriber
*/
/**
* Sent when the subscriber's stream has been interrupted.
*
* In response to this event, you may want to provide a user interface notification, to let the * user know that the audio-video stream is temporarily disconnected and that the app is trying * to reconnect to it. *
* If the client reconnects to the stream, the Subscriber object dispatches a
* connected event. Otherwise, if the client cannot reconnect to the stream,
* the Subscriber object dispatches a destroyed event.
*
* This event is part of the automatic reconnection beta feature. To participate in the beta
* program, see the OpenTok Beta programs
* page.
*
* @name disconnected
* @event
* @memberof Subscriber
* @see connected event
* @see Event
*/
/**
* Sent when the subscriber's stream has resumed, after the Subscriber dispatches a
* disconnected event.
*
* This event is part of the automatic reconnection beta feature. To participate in the beta
* program, see the OpenTok Beta programs
* page.
*
* @name connected
* @event
* @memberof Subscriber
* @see disconnected event
* @see Event
*/
/**
* Dispatched when the Subscriber element is removed from the HTML DOM. When this event is
* dispatched, you may choose to adjust or remove HTML DOM elements related to the subscriber.
* @see Event
* @name destroyed
* @event
* @memberof Subscriber
*/
/**
* Dispatched when the video dimensions of the video change. This can occur when the
* stream.videoType property is set to "screen" (for a screen-sharing
* video stream), when the user resizes the window being captured. It can also occur if the video
* is being published by a mobile device and the user rotates the device (causing the camera
* orientation to change). This event object has a newValue property and an
* oldValue property, representing the new and old dimensions of the video.
* Each of these has a height property and a width property,
* representing the height and width, in pixels.
* @name videoDimensionsChanged
* @event
* @memberof Subscriber
* @see VideoDimensionsChangedEvent
*/
};
Subscriber.WidgetView = WidgetView;
Subscriber.SubscriberPeerConnection = SubscriberPeerConnection;
module.exports = Subscriber;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"../../helpers/audio_context.js":141,"../../helpers/audio_level_samplers/getstats_audio_output_level_sampler":142,"../../helpers/audio_level_samplers/webaudio_audio_level_sampler":143,"../../helpers/connectivity_attempt_pinger.js":145,"../../helpers/properties.js":154,"../../helpers/widget_view.js":164,"../analytics.js":166,"../audio_level_transformer":170,"../chrome/audio_level_meter.js":173,"../chrome/backing_bar.js":174,"../chrome/chrome.js":176,"../chrome/mute_button.js":177,"../chrome/name_panel.js":178,"../chrome/video_disabled_indicator.js":179,"../events.js":182,"../exception_codes.js":183,"../interval_runner.js":186,"../logging.js":187,"../messaging/raptor/message.js":192,"../ot_error.js":208,"../peer_connection/get_stats_helpers.js":211,"../peer_connection/subscriber_peer_connection.js":223,"../styling/stylable_component.js":244,"./state.js":248,"@opentok/ot-helpers":4,"@opentok/otplugin.js":40,"events":72,"uuid":137}],248:[function(require,module,exports){
'use strict';
var generateSimpleStateMachine = require('../generate_simple_state_machine.js');
// Models a Subscriber's subscribing State
//
// Valid States:
// NotSubscribing (the initial state
// Init (basic setup of DOM
// ConnectingToPeer (Failure Cases -> No Route, Bad Offer, Bad Answer
// BindingRemoteStream (Failure Cases -> Anything to do with the media being
// (invalid, the media never plays
// Subscribing (this is 'onLoad'
// Failed (terminal state, with a reason that maps to one of the
// (failure cases above
// Destroyed (The subscriber has been cleaned up, terminal state
//
//
// Valid Transitions:
// NotSubscribing ->
// Init
//
// Init ->
// ConnectingToPeer
// | BindingRemoteStream (if we are subscribing to ourselves and we alreay
// (have a stream
// | NotSubscribing (destroy()
//
// ConnectingToPeer ->
// BindingRemoteStream
// | NotSubscribing
// | Failed
// | NotSubscribing (destroy()
//
// BindingRemoteStream ->
// Subscribing
// | Failed
// | NotSubscribing (destroy()
//
// Subscribing ->
// NotSubscribing (unsubscribe
// | Failed (probably a peer connection failure after we began
// (subscribing
//
// Failed ->
// Destroyed
//
// Destroyed -> (terminal state)
//
//
// @example
// var state = new SubscribingState(function(change) {
// console.log(change.message);
// });
//
// state.set('Init');
// state.current; -> 'Init'
//
// state.set('Subscribing'); -> triggers stateChangeFailed and logs out the error message
//
//
var initialState = 'NotSubscribing';
var validStates = [
'NotSubscribing', 'Init', 'ConnectingToPeer',
'BindingRemoteStream', 'Subscribing', 'Failed',
'Destroyed'
];
var validTransitions = {
NotSubscribing: ['NotSubscribing', 'Init', 'Destroyed'],
Init: ['NotSubscribing', 'ConnectingToPeer', 'BindingRemoteStream', 'Destroyed'],
ConnectingToPeer: ['NotSubscribing', 'BindingRemoteStream', 'Failed', 'Destroyed'],
BindingRemoteStream: ['NotSubscribing', 'Subscribing', 'Failed', 'Destroyed'],
Subscribing: ['NotSubscribing', 'Failed', 'Destroyed'],
Failed: ['Destroyed'],
Destroyed: []
};
var SubscribingState = generateSimpleStateMachine(initialState, validStates, validTransitions);
SubscribingState.prototype.isDestroyed = function() {
return this.current === 'Destroyed';
};
SubscribingState.prototype.isFailed = function() {
return this.current === 'Failed';
};
SubscribingState.prototype.isSubscribing = function() {
return this.current === 'Subscribing';
};
SubscribingState.prototype.isAttemptingToSubscribe = function() {
return [
'Init',
'ConnectingToPeer',
'BindingRemoteStream'
].indexOf(this.current) !== -1;
};
module.exports = SubscribingState;
},{"../generate_simple_state_machine.js":184}],249:[function(require,module,exports){
(function (global){
'use strict';
var analytics = require('./analytics.js');
var APIKEY = require('./api_key.js');
var Dialogs = require('../helpers/dialogs.js');
var EnvironmentLoader = require('../ot/environment_loader.js');
var OTPlugin = require('@opentok/otplugin.js');
var OTHelpers = require('@opentok/ot-helpers');
var logging = require('./logging.js');
var properties = require('../helpers/properties.js');
var systemRequirements = {};
module.exports = systemRequirements;
// Global parameters used by upgradeSystemRequirements
var _intervalId;
var _lastHash = document.location.hash;
var HAS_REQUIREMENTS = 1;
var NOT_HAS_REQUIREMENTS = 0;
/**
* Checks if the system supports OpenTok for WebRTC. Note that this method is called
* automatically when you call OT.initPublisher() or OT.initSession(),
* and if the system doesn't support OpenTok, the OpenTok.js library displays a message to
* the user. Call the OT.checkSystemRequirements() method before calling
* OT.initPublisher() or OT.initSession() if you do not want the
* library to display that message.
*
* @return {Number} Whether the system supports OpenTok for WebRTC (1) or not (0).
* @see OT.upgradeSystemRequirements()
* @method OT.checkSystemRequirements
* @memberof OT
*/
systemRequirements.check = function() {
logging.debug('OT.checkSystemRequirements()');
// Try native support first, then OTPlugin...
var systemRequirementsMet = OTHelpers.hasCapabilities('websockets', 'webrtc') ||
OTPlugin.isInstalled();
systemRequirementsMet = systemRequirementsMet ?
HAS_REQUIREMENTS : NOT_HAS_REQUIREMENTS;
systemRequirements.check = function() {
logging.debug('OT.checkSystemRequirements()');
return systemRequirementsMet;
};
if (systemRequirementsMet === NOT_HAS_REQUIREMENTS) {
analytics.logEvent({
action: 'checkSystemRequirements',
variation: 'notHasRequirements',
partnerId: APIKEY.value,
payload: { userAgent: OTHelpers.env.userAgent }
});
}
return systemRequirementsMet;
};
/**
* Displays information about system requirments for OpenTok for WebRTC. This
* information is displayed in an iframe element that fills the browser window.
*
* Note: this information is displayed automatically when you call the
* OT.initSession() or the OT.initPublisher() method
* if the client does not support OpenTok for WebRTC.
*